Files
2026-05-18 23:50:50 -05:00

263 lines
8.5 KiB
Go
Executable File

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/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// 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 := "✅ <b>Prueba de conexión</b>\nEsta es una notificación de prueba desde <b>u-site admin</b>."
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("<b>%s</b>\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"})
}
webhookURL := services.GetPublicURL() + "/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)
}