This commit is contained in:
parent
9c78ea2d48
commit
7c684af8c3
79 changed files with 3594 additions and 3257 deletions
262
internal/platform/telegram/telegram.go
Normal file
262
internal/platform/telegram/telegram.go
Normal file
|
@ -0,0 +1,262 @@
|
|||
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue