package controllers 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" ) // TelegramIndex renderiza la vista de configuración de Telegram. func TelegramIndex(c *fiber.Ctx) error { return c.Render("telegram", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } // GetTelegramConfigs devuelve todas las configs en JSON. func GetTelegramConfigs(c *fiber.Ctx) error { items, err := models.GetAllTelegramConfigs() if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(items) } // CreateTelegramConfig crea una nueva configuración. func CreateTelegramConfig(c *fiber.Ctx) error { var m models.TelegramConfig if err := c.BodyParser(&m); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if strings.TrimSpace(m.Nombre) == "" || strings.TrimSpace(m.BotToken) == "" || strings.TrimSpace(m.ChatID) == "" { return c.Status(400).JSON(fiber.Map{"error": "nombre, bot_token y chat_id son obligatorios"}) } m.Activo = true if err := models.CreateTelegramConfig(&m); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(m) } // UpdateTelegramConfig actualiza una configuración. func UpdateTelegramConfig(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"}) } var m models.TelegramConfig if err := c.BodyParser(&m); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } m.ID = uint(id) // Si no se envía bot_token, mantener el existente if strings.TrimSpace(m.BotToken) == "" { cfg, err := models.GetTelegramConfigByID(uint(id)) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "no encontrado"}) } m.BotToken = cfg.BotToken } if err := models.UpdateTelegramConfig(&m); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // DeleteTelegramConfig elimina una configuración. func DeleteTelegramConfig(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"}) } if err := models.DeleteTelegramConfig(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // TestTelegramConfig envía un mensaje de prueba al bot configurado. func TestTelegramConfig(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"}) } msg := "✅ Prueba de conexión\nEsta es una notificación de prueba desde u-site admin." sendErr := sendTelegramMessage(cfg.BotToken, cfg.ChatID, msg) logEntry := &models.TelegramLog{ TelegramConfigID: cfg.ID, Titulo: "Prueba manual", Mensaje: msg, Estado: "ok", } if sendErr != nil { logEntry.Estado = "failed" logEntry.ErrorMsg = sendErr.Error() _ = models.CreateTelegramLog(logEntry) return c.Status(422).JSON(fiber.Map{"ok": false, "error": sendErr.Error()}) } _ = models.CreateTelegramLog(logEntry) return c.JSON(fiber.Map{"ok": true, "message": "Mensaje enviado"}) } // SendTelegramNotification envía un mensaje personalizado a una o varias configs. // Body: { "config_ids": [1,2], "titulo": "...", "mensaje": "..." } func SendTelegramNotification(c *fiber.Ctx) error { type Req struct { ConfigIDs []uint `json:"config_ids"` Titulo string `json:"titulo"` Mensaje string `json:"mensaje"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if strings.TrimSpace(req.Mensaje) == "" { return c.Status(400).JSON(fiber.Map{"error": "El mensaje no puede estar vacío"}) } if len(req.ConfigIDs) == 0 { return c.Status(400).JSON(fiber.Map{"error": "Selecciona al menos un destino"}) } text := req.Mensaje if req.Titulo != "" { text = fmt.Sprintf("%s\n\n%s", req.Titulo, req.Mensaje) } var enviados, fallidos int for _, cid := range req.ConfigIDs { cfg, err := models.GetTelegramConfigByID(cid) if err != nil || !cfg.Activo { fallidos++ continue } sendErr := sendTelegramMessage(cfg.BotToken, cfg.ChatID, text) logEntry := &models.TelegramLog{ TelegramConfigID: cfg.ID, Titulo: req.Titulo, Mensaje: req.Mensaje, Estado: "ok", } if sendErr != nil { logEntry.Estado = "failed" logEntry.ErrorMsg = sendErr.Error() fallidos++ } else { enviados++ } _ = models.CreateTelegramLog(logEntry) } return c.JSON(fiber.Map{"ok": fallidos == 0, "enviados": enviados, "fallidos": fallidos}) } // GetTelegramLogs devuelve el historial paginado de mensajes enviados. func GetTelegramLogs(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) if page < 1 { page = 1 } limit := 30 offset := (page - 1) * limit items, total, err := models.GetTelegramLogs(limit, offset) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } // ─── helper interno ─────────────────────────────────────────────────────────── func sendTelegramMessage(botToken, chatID, text string) error { if botToken == "" { return fmt.Errorf("bot_token vacío") } apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken) payload := map[string]interface{}{ "chat_id": chatID, "text": text, "parse_mode": "HTML", } body, _ := json.Marshal(payload) resp, err := http.Post(apiURL, "application/json", bytes.NewReader(body)) //nolint:noctx if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Telegram respondió %d", resp.StatusCode) } 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) }