package telegram import ( "bytes" "encoding/json" "errors" "fmt" "io" "log/slog" "net/http" "os" "strconv" "time" "git.nakama.town/fmartingr/butterrobot/internal/config" "git.nakama.town/fmartingr/butterrobot/internal/model" ) // TelegramPlatform implements the Platform interface for Telegram type TelegramPlatform struct { config *config.TelegramConfig apiURL string log *slog.Logger } // New creates a new TelegramPlatform instance func New(cfg *config.TelegramConfig) *TelegramPlatform { return &TelegramPlatform{ config: cfg, apiURL: "https://api.telegram.org/bot" + cfg.Token, log: slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})).With(slog.String("platform", "telegram")), } } // Init initializes the Telegram platform func (t *TelegramPlatform) Init(cfg *config.Config) error { if t.config.Token == "" { t.log.Error("Missing Telegram token") return model.ErrPlatformInit } // Set webhook URL based on hostname webhookURL := fmt.Sprintf("https://%s/telegram/incoming/%s", cfg.Hostname, t.config.Token) t.log.Info("Setting Telegram webhook", "url", webhookURL) // Create webhook setup request url := fmt.Sprintf("%s/setWebhook", t.apiURL) payload := map[string]interface{}{ "url": webhookURL, "max_connections": 40, "allowed_updates": []string{"message"}, } data, err := json.Marshal(payload) if err != nil { t.log.Error("Failed to marshal webhook payload", "error", err) return fmt.Errorf("failed to marshal webhook payload: %w", err) } resp, err := http.Post(url, "application/json", bytes.NewBuffer(data)) if err != nil { t.log.Error("Failed to set webhook", "error", err) return fmt.Errorf("failed to set webhook: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) errMsg := string(bodyBytes) t.log.Error("Telegram API error", "status", resp.StatusCode, "response", errMsg) return fmt.Errorf("telegram API error: %d - %s", resp.StatusCode, errMsg) } t.log.Info("Telegram webhook successfully set") return nil } // ParseIncomingMessage parses an incoming Telegram message func (t *TelegramPlatform) ParseIncomingMessage(r *http.Request) (*model.Message, error) { t.log.Debug("Parsing incoming Telegram message") // Read request body body, err := io.ReadAll(r.Body) if err != nil { t.log.Error("Failed to read request body", "error", err) return nil, err } defer r.Body.Close() // Parse JSON var update struct { Message struct { MessageID int `json:"message_id"` From struct { ID int `json:"id"` IsBot bool `json:"is_bot"` Username string `json:"username"` FirstName string `json:"first_name"` } `json:"from"` Chat struct { ID int64 `json:"id"` Type string `json:"type"` Title string `json:"title,omitempty"` Username string `json:"username,omitempty"` } `json:"chat"` Date int `json:"date"` Text string `json:"text"` } `json:"message"` } if err := json.Unmarshal(body, &update); err != nil { t.log.Error("Failed to unmarshal update", "error", err) return nil, err } // Convert to raw map for storage var raw map[string]interface{} if err := json.Unmarshal(body, &raw); err != nil { t.log.Error("Failed to unmarshal raw data", "error", err) return nil, err } // Create message msg := &model.Message{ Text: update.Message.Text, Chat: strconv.FormatInt(update.Message.Chat.ID, 10), Author: update.Message.From.Username, FromBot: update.Message.From.IsBot, Date: time.Unix(int64(update.Message.Date), 0), ID: strconv.Itoa(update.Message.MessageID), Raw: raw, } t.log.Debug("Parsed message", "id", msg.ID, "chat", msg.Chat, "author", msg.Author, "from_bot", msg.FromBot, "text_length", len(msg.Text)) // Create Channel object channelRaw, err := t.ParseChannelFromMessage(body) if err != nil { t.log.Error("Failed to parse channel data", "error", err) return nil, err } msg.Channel = &model.Channel{ Platform: "telegram", PlatformChannelID: msg.Chat, ChannelRaw: channelRaw, } return msg, nil } // ParseChannelNameFromRaw extracts a human-readable channel name from raw data func (t *TelegramPlatform) ParseChannelNameFromRaw(channelRaw map[string]interface{}) string { // Try to get the title first (for groups) if chatInfo, ok := channelRaw["chat"].(map[string]interface{}); ok { if title, ok := chatInfo["title"].(string); ok && title != "" { return title } // For private chats, use username if username, ok := chatInfo["username"].(string); ok && username != "" { return username } // Fallback to first_name if available if firstName, ok := chatInfo["first_name"].(string); ok && firstName != "" { return firstName } // Last resort: use the ID if id, ok := chatInfo["id"].(float64); ok { return strconv.FormatInt(int64(id), 10) } } return "unknown" } // ParseChannelFromMessage extracts channel data from a message func (t *TelegramPlatform) ParseChannelFromMessage(body []byte) (map[string]any, error) { // Parse JSON to extract chat info var update struct { Message struct { Chat map[string]any `json:"chat"` } `json:"message"` } if err := json.Unmarshal(body, &update); err != nil { return nil, err } if update.Message.Chat == nil { return nil, errors.New("chat information not found") } return map[string]any{ "chat": update.Message.Chat, }, nil } // SendMessage sends a message to Telegram func (t *TelegramPlatform) SendMessage(msg *model.Message) error { // Convert chat ID to int64 chatID, err := strconv.ParseInt(msg.Chat, 10, 64) if err != nil { t.log.Error("Failed to parse chat ID", "chat", msg.Chat, "error", err) return err } // Prepare payload payload := map[string]interface{}{ "chat_id": chatID, "text": msg.Text, } // Add reply if needed if msg.ReplyTo != "" { replyToID, err := strconv.Atoi(msg.ReplyTo) if err == nil { payload["reply_to_message_id"] = replyToID } else { t.log.Warn("Failed to parse reply_to ID", "reply_to", msg.ReplyTo, "error", err) } } t.log.Debug("Sending message to Telegram", "chat_id", chatID, "length", len(msg.Text)) // Convert payload to JSON data, err := json.Marshal(payload) if err != nil { t.log.Error("Failed to marshal message payload", "error", err) return err } // Send HTTP request resp, err := http.Post( t.apiURL+"/sendMessage", "application/json", bytes.NewBuffer(data), ) if err != nil { t.log.Error("Failed to send message", "error", err) return err } defer resp.Body.Close() // Check response if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) errMsg := string(bodyBytes) t.log.Error("Telegram API error", "status", resp.StatusCode, "response", errMsg) return fmt.Errorf("telegram API error: %d - %s", resp.StatusCode, errMsg) } t.log.Debug("Message sent successfully") return nil }