278 lines
7.4 KiB
Go
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
|
|
}
|