package services import ( "bytes" "encoding/json" "fmt" "net/http" "os" "time" ) // telegramHTTPClient evita que una llamada colgada a Telegram deje la // goroutine bloqueada indefinidamente (http.Get/Post no tienen timeout). var telegramHTTPClient = &http.Client{Timeout: 20 * time.Second} type TelegramService struct { BotToken string // exportado para uso desde otros paquetes } type TelegramMessage struct { ChatID interface{} `json:"chat_id"` Text string `json:"text"` ParseMode string `json:"parse_mode"` } func NewTelegramService() *TelegramService { botToken := os.Getenv("TELEGRAM_BOT_TOKEN") if botToken == "" { fmt.Println("Warning: TELEGRAM_BOT_TOKEN not set in environment variables") } return &TelegramService{BotToken: botToken} } func (ts *TelegramService) SendMessage(chatID interface{}, message string) error { if ts.BotToken == "" { return fmt.Errorf("Telegram bot token is not configured") } apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", ts.BotToken) telegramMessage := TelegramMessage{ ChatID: chatID, Text: message, ParseMode: "HTML", } jsonData, err := json.Marshal(telegramMessage) if err != nil { return fmt.Errorf("error marshalling request body: %v", err) } resp, err := telegramHTTPClient.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("error sending request to Telegram API: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("received non-OK response from Telegram API: %d", resp.StatusCode) } return nil } // GetBotUsername obtiene el username (@nombre) del bot a partir de su token. func GetBotUsername(botToken string) string { if botToken == "" { return "" } resp, err := telegramHTTPClient.Get(fmt.Sprintf("https://api.telegram.org/bot%s/getMe", botToken)) if err != nil { return "" } defer resp.Body.Close() var result struct { OK bool `json:"ok"` Result struct { Username string `json:"username"` } `json:"result"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "" } return result.Result.Username } // SendMessageWithToken envía un mensaje usando un bot token explícito (útil para notificar a usuarios con su propio chat_id). func (ts *TelegramService) SendMessageWithToken(chatID interface{}, message, botToken string) error { svc := &TelegramService{BotToken: botToken} return svc.SendMessage(chatID, message) } // RemitenteReciente es alguien que le escribió al bot recientemente, útil para // descubrir su chat_id sin tener que pedírselo por otro medio. type RemitenteReciente struct { ChatID int64 Nombre string Mensaje string } // UpdatesRecientesDelBot consulta getUpdates y devuelve, más reciente primero, // los remitentes que le han escrito al bot (hasta los últimos 100 updates que // Telegram todavía tenga en cola). func UpdatesRecientesDelBot(botToken string) ([]RemitenteReciente, error) { if botToken == "" { return nil, fmt.Errorf("bot token vacío") } resp, err := telegramHTTPClient.Get(fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates?limit=100", botToken)) //nolint:noctx if err != nil { return nil, fmt.Errorf("no se pudo consultar Telegram: %w", err) } defer resp.Body.Close() var result struct { OK bool `json:"ok"` Result []struct { Message struct { From struct { FirstName string `json:"first_name"` LastName string `json:"last_name"` Username string `json:"username"` } `json:"from"` 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 nil, fmt.Errorf("Telegram no devolvió updates válidos") } out := make([]RemitenteReciente, 0, len(result.Result)) for i := len(result.Result) - 1; i >= 0; i-- { m := result.Result[i].Message if m.Chat.ID == 0 { continue } nombre := m.From.FirstName if m.From.LastName != "" { nombre += " " + m.From.LastName } if m.From.Username != "" { nombre += " (@" + m.From.Username + ")" } if nombre == "" { nombre = fmt.Sprintf("Chat %d", m.Chat.ID) } out = append(out, RemitenteReciente{ChatID: m.Chat.ID, Nombre: nombre, Mensaje: m.Text}) } return out, nil }