feat: enviar factura por Telegram como documento adjunto
El bot solo procesaba mensajes de texto: un documento o foto enviado al chat se ignoraba por completo (Telegram lo manda en message.document/photo con caption, no en el campo text). Ahora el webhook detecta el adjunto, lo descarga desde la API de Telegram y lo deja pendiente para el chat; el agente usa la nueva tool adjuntar_factura para asociarlo a un cliente (preguntando cliente/monto si el caption no los trae) y crear la Factura con el archivo ya vinculado, igual que si se subiera desde el dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8fce587304
commit
b292d98cf6
@@ -0,0 +1,102 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FacturaAttachment es un archivo que un usuario envió por Telegram y que queda
|
||||
// "pendiente" hasta que el agente lo asocie a una factura (o expire).
|
||||
type FacturaAttachment struct {
|
||||
Path string
|
||||
OriginalName string
|
||||
MimeType string
|
||||
StagedAt time.Time
|
||||
}
|
||||
|
||||
const facturaAttachmentTTL = 15 * time.Minute
|
||||
|
||||
var (
|
||||
facturaAttachmentsMu sync.Mutex
|
||||
facturaAttachments = map[int64]*FacturaAttachment{}
|
||||
)
|
||||
|
||||
// DescargarDocumentoTelegram descarga un archivo de Telegram (por file_id) usando
|
||||
// la API getFile + el endpoint de descarga, y lo deja en un directorio temporal.
|
||||
func DescargarDocumentoTelegram(botToken, fileID, originalName, mimeType string) (string, error) {
|
||||
getFileURL := fmt.Sprintf("https://api.telegram.org/bot%s/getFile?file_id=%s", botToken, fileID)
|
||||
resp, err := http.Get(getFileURL) //nolint:noctx
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no se pudo consultar el archivo en Telegram: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
OK bool `json:"ok"`
|
||||
Result struct {
|
||||
FilePath string `json:"file_path"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || !result.OK || result.Result.FilePath == "" {
|
||||
return "", fmt.Errorf("Telegram no devolvió la ruta del archivo")
|
||||
}
|
||||
|
||||
downloadURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", botToken, result.Result.FilePath)
|
||||
fresp, err := http.Get(downloadURL) //nolint:noctx
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no se pudo descargar el archivo: %w", err)
|
||||
}
|
||||
defer fresp.Body.Close()
|
||||
|
||||
dir := "uploads/facturas/tmp"
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return "", fmt.Errorf("no se pudo crear el directorio temporal: %w", err)
|
||||
}
|
||||
ext := filepath.Ext(originalName)
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(result.Result.FilePath)
|
||||
}
|
||||
localPath := filepath.Join(dir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext))
|
||||
out, err := os.Create(localPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no se pudo guardar el archivo: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
if _, err := io.Copy(out, fresp.Body); err != nil {
|
||||
return "", fmt.Errorf("error escribiendo el archivo: %w", err)
|
||||
}
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
// StageFacturaAttachment registra un archivo ya descargado como pendiente para el
|
||||
// chat, a la espera de que el agente lo asocie a una factura con la tool adjuntar_factura.
|
||||
func StageFacturaAttachment(chatID int64, path, originalName, mimeType string) {
|
||||
facturaAttachmentsMu.Lock()
|
||||
defer facturaAttachmentsMu.Unlock()
|
||||
facturaAttachments[chatID] = &FacturaAttachment{
|
||||
Path: path, OriginalName: originalName, MimeType: mimeType, StagedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// PopFacturaAttachment retorna y remueve el archivo pendiente de un chat, si
|
||||
// existe y no expiró (si expiró, borra el archivo temporal del disco).
|
||||
func PopFacturaAttachment(chatID int64) (*FacturaAttachment, bool) {
|
||||
facturaAttachmentsMu.Lock()
|
||||
defer facturaAttachmentsMu.Unlock()
|
||||
a, ok := facturaAttachments[chatID]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
delete(facturaAttachments, chatID)
|
||||
if time.Since(a.StagedAt) > facturaAttachmentTTL {
|
||||
_ = os.Remove(a.Path)
|
||||
return nil, false
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
Reference in New Issue
Block a user