package services import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // ProcesarMensajeTelegramUmind adapta un mensaje entrante del canal Telegram // de un agente al mismo motor que atiende el widget web // (ProcessWidgetMessage) y responde usando el bot token propio del canal // (no el bot interno de staff). La sesión se separa por chat_id con un // prefijo para no colisionar con session_ids del widget. func ProcesarMensajeTelegramUmind(canal *models.UmindCanal, chatID int64, texto string) error { agente, err := models.GetUmindAgenteByID(canal.AgenteID) if err != nil || !agente.Activo { return fmt.Errorf("agente no encontrado o inactivo: %w", err) } credenciales, err := DescifrarCredencialesCanal(canal.CredencialesEnc) if err != nil { return fmt.Errorf("credenciales del canal corruptas: %w", err) } botToken := credenciales["bot_token"] if botToken == "" { return fmt.Errorf("el canal no tiene bot_token configurado") } sessionID := fmt.Sprintf("tg:%d", chatID) respuesta, err := ProcessWidgetMessage(agente, sessionID, texto) if err != nil { return fmt.Errorf("error del agente: %w", err) } return (&TelegramService{}).SendMessageWithToken(chatID, respuesta, botToken) } // ProcesarMediaTelegramUmind atiende notas de voz/audio y fotos entrantes: // si el canal tiene la conversión habilitada, descarga el archivo vía // getFile, lo pasa por Whisper/OCR y responde igual que un mensaje de texto. // Si no está habilitada, se ignora en silencio. func ProcesarMediaTelegramUmind(canal *models.UmindCanal, chatID int64, fileID, tipo string) error { return ProcesarMediaTelegramUmindConNombre(canal, chatID, fileID, tipo, "", "") } // ProcesarMediaTelegramUmindConNombre es la versión completa: los documentos // traen nombre de archivo y, a veces, un texto que los acompaña (caption). func ProcesarMediaTelegramUmindConNombre(canal *models.UmindCanal, chatID int64, fileID, tipo, nombreArchivo, caption string) error { agente, err := models.GetUmindAgenteByID(canal.AgenteID) if err != nil || !agente.Activo { return fmt.Errorf("agente no encontrado o inactivo: %w", err) } credenciales, err := DescifrarCredencialesCanal(canal.CredencialesEnc) if err != nil { return fmt.Errorf("credenciales del canal corruptas: %w", err) } botToken := credenciales["bot_token"] if botToken == "" { return fmt.Errorf("el canal no tiene bot_token configurado") } var texto string switch tipo { case "audio": if !canal.UsarWhisperAudio { return nil } data, err := descargarArchivoTelegram(botToken, fileID) if err != nil { return fmt.Errorf("no se pudo descargar el audio de Telegram: %w", err) } texto, err = TranscribirAudioSelfHosted(canal.AgenteID, data, "audio.ogg") if err != nil { return err } case "image": if !canal.UsarOcrImagenes { return nil } data, err := descargarArchivoTelegram(botToken, fileID) if err != nil { return fmt.Errorf("no se pudo descargar la imagen de Telegram: %w", err) } texto, err = ExtraerTextoOCR(canal.AgenteID, data, "image/jpeg") if err != nil { return err } case "document": if !canal.UsarArchivosDocs { return nil } data, err := descargarArchivoTelegram(botToken, fileID) if err != nil { return fmt.Errorf("no se pudo descargar el archivo de Telegram: %w", err) } texto, err = ExtraerTextoDeArchivo(canal.AgenteID, nombreArchivo, data) if err != nil { return err } texto = TextoDeArchivoParaAgente(nombreArchivo, caption, texto) default: return nil } if strings.TrimSpace(texto) == "" { return fmt.Errorf("no se pudo extraer texto del %s recibido", tipo) } sessionID := fmt.Sprintf("tg:%d", chatID) respuesta, err := ProcessWidgetMessage(agente, sessionID, texto) if err != nil { return fmt.Errorf("error del agente: %w", err) } return (&TelegramService{}).SendMessageWithToken(chatID, respuesta, botToken) } // descargarArchivoTelegram resuelve el file_path de un file_id (getFile) y // descarga el archivo desde el CDN de archivos de Telegram. func descargarArchivoTelegram(botToken, fileID string) ([]byte, error) { getFileURL := fmt.Sprintf("https://api.telegram.org/bot%s/getFile?file_id=%s", botToken, url.QueryEscape(fileID)) resp, err := telegramHTTPClient.Get(getFileURL) if err != nil { return nil, fmt.Errorf("no se pudo consultar getFile: %w", err) } defer resp.Body.Close() var out struct { Result struct { FilePath string `json:"file_path"` } `json:"result"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil || out.Result.FilePath == "" { return nil, fmt.Errorf("respuesta inesperada de getFile") } fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", botToken, out.Result.FilePath) resp2, err := telegramHTTPClient.Get(fileURL) if err != nil { return nil, fmt.Errorf("no se pudo descargar el archivo: %w", err) } defer resp2.Body.Close() return io.ReadAll(io.LimitReader(resp2.Body, 20*1024*1024)) } // RegistrarWebhookTelegram le dice a Telegram a qué URL mandar los updates // del bot — se llama una vez al crear el canal (o al reconfigurar el token). func RegistrarWebhookTelegram(botToken, webhookURL string) error { if botToken == "" || webhookURL == "" { return fmt.Errorf("bot_token y webhookURL son requeridos") } api := fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook?url=%s", botToken, url.QueryEscape(webhookURL)) resp, err := telegramHTTPClient.Get(api) if err != nil { return fmt.Errorf("no se pudo contactar la API de Telegram: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Telegram respondió %d al registrar el webhook", resp.StatusCode) } return nil }