butterrobot/internal/platform/telegram/telegram.go
Felipe M. c9edb57505
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
fix: make format
2025-04-22 11:56:57 +02:00

278 lines
7.4 KiB
Go

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 func() {
if err := resp.Body.Close(); err != nil {
t.log.Error("Error closing response body", "error", err)
}
}()
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 func() {
if err := r.Body.Close(); err != nil {
t.log.Error("Error closing request body", "error", err)
}
}()
// 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"`
ReplyToMessage struct {
MessageID int `json:"message_id"`
} `json:"reply_to_message"`
} `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),
ReplyTo: strconv.Itoa(update.Message.ReplyToMessage.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 func() {
if err := resp.Body.Close(); err != nil {
t.log.Error("Error closing response body", "error", err)
}
}()
// 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
}