Files
Cyrene/backend/platform-bridge/internal/adapter/telegram/adapter.go
T

252 lines
6.8 KiB
Go

package telegram
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/yourname/cyrene-ai/platform-bridge/internal/bridge"
)
// Adapter implements PlatformAdapter for Telegram Bot API.
type Adapter struct {
token string
webhookURL string
client *http.Client
connected bool
}
func NewAdapter(token, webhookURL string) *Adapter {
return &Adapter{
token: token,
webhookURL: webhookURL,
client: &http.Client{Timeout: 10 * time.Second},
}
}
func (a *Adapter) PlatformName() string { return "telegram" }
func (a *Adapter) Capabilities() bridge.PlatformCapabilities {
return bridge.PlatformCapabilities{
MaxMessageLength: 4096,
SupportsMarkdown: true,
SupportsImage: true,
SupportsVoice: true,
SupportsEmoji: true,
SupportsReaction: true,
SupportsTypingHint: true,
RecommendBurstMax: 3,
}
}
func (a *Adapter) Connect(ctx context.Context) error {
if a.token == "" {
return fmt.Errorf("telegram bot token not configured")
}
if a.webhookURL == "" {
// Polling mode not implemented; webhook required.
return fmt.Errorf("telegram webhook URL not configured")
}
// Set webhook.
url := fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook?url=%s/api/v1/webhook/telegram", a.token, a.webhookURL)
resp, err := a.client.Post(url, "application/json", nil)
if err != nil {
return fmt.Errorf("failed to set telegram webhook: %w", err)
}
defer resp.Body.Close()
a.connected = true
fmt.Println("[telegram] webhook set, bot ready")
return nil
}
func (a *Adapter) Disconnect(ctx context.Context) error {
if a.token != "" {
url := fmt.Sprintf("https://api.telegram.org/bot%s/deleteWebhook", a.token)
a.client.Post(url, "application/json", nil)
}
a.connected = false
return nil
}
func (a *Adapter) IsConnected() bool { return a.connected }
func (a *Adapter) HealthCheck() error {
if a.token == "" {
return fmt.Errorf("telegram token not configured")
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/getMe", a.token)
resp, err := a.client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("telegram API returned %d", resp.StatusCode)
}
return nil
}
// TelegramUpdate is the webhook payload from Telegram.
type TelegramUpdate struct {
UpdateID int64 `json:"update_id"`
Message *TelegramMessage `json:"message"`
Callback *TelegramCallback `json:"callback_query"`
}
// TelegramMessage represents a Telegram message.
type TelegramMessage struct {
MessageID int64 `json:"message_id"`
From TelegramUser `json:"from"`
Chat TelegramChat `json:"chat"`
Text string `json:"text"`
Date int64 `json:"date"`
ReplyTo *TelegramMessage `json:"reply_to_message"`
Entities []TelegramEntity `json:"entities"`
}
// TelegramUser represents a Telegram user.
type TelegramUser struct {
ID int64 `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// TelegramChat represents a Telegram chat.
type TelegramChat struct {
ID int64 `json:"id"`
Type string `json:"type"` // "private", "group", "supergroup", "channel"
}
// TelegramEntity represents a text entity in a Telegram message.
type TelegramEntity struct {
Type string `json:"type"` // "mention", "bot_command", etc.
Offset int `json:"offset"`
Length int `json:"length"`
}
// TelegramCallback represents a callback query.
type TelegramCallback struct {
ID string `json:"id"`
From TelegramUser `json:"from"`
Message *TelegramMessage `json:"message"`
Data string `json:"data"`
}
// ToUnified converts a Telegram update to UnifiedMessage.
// Accepts both *TelegramUpdate and map[string]interface{} (from webhook).
func (a *Adapter) ToUnified(rawMessage interface{}) (*bridge.UnifiedMessage, error) {
var msg *TelegramMessage
switch v := rawMessage.(type) {
case *TelegramUpdate:
msg = v.Message
case map[string]interface{}:
// Parse from raw webhook payload.
data, _ := json.Marshal(v)
var update TelegramUpdate
if err := json.Unmarshal(data, &update); err != nil {
return nil, fmt.Errorf("parse telegram update: %w", err)
}
msg = update.Message
default:
return nil, fmt.Errorf("expected *TelegramUpdate or map, got %T", rawMessage)
}
if msg == nil {
return nil, fmt.Errorf("no message in update")
}
channelType := "direct"
channelID := fmt.Sprintf("%d", msg.Chat.ID)
if msg.Chat.Type == "group" || msg.Chat.Type == "supergroup" {
channelType = "group"
} else if msg.Chat.Type == "channel" {
channelType = "channel"
}
senderName := msg.From.FirstName
if msg.From.LastName != "" {
senderName += " " + msg.From.LastName
}
if msg.From.Username != "" {
senderName = msg.From.Username
}
replyTo := ""
if msg.ReplyTo != nil {
replyTo = fmt.Sprintf("%d", msg.ReplyTo.MessageID)
}
return &bridge.UnifiedMessage{
SenderID: fmt.Sprintf("%d", msg.From.ID),
SenderName: senderName,
Platform: "telegram",
ChannelID: channelID,
ChannelType: channelType,
Content: msg.Text,
ContentType: "text",
MessageID: fmt.Sprintf("%d", msg.MessageID),
ReplyTo: replyTo,
RawData: rawMessage,
Timestamp: time.Unix(msg.Date, 0),
}, nil
}
// FromUnified converts a unified response to Telegram messages.
func (a *Adapter) FromUnified(response *bridge.UnifiedResponse) ([]bridge.PlatformMessage, error) {
var msgs []bridge.PlatformMessage
for _, rm := range response.Messages {
content := rm.Content
format := rm.FormatMode
if format == "" {
format = "markdown"
}
msgs = append(msgs, bridge.PlatformMessage{
Content: content,
FormatMode: format,
ReplyTo: response.ReplyTo,
})
}
return msgs, nil
}
// SendMessage sends a message via Telegram Bot API.
func (a *Adapter) SendMessage(chatID int64, text, parseMode, replyTo string) error {
body := map[string]interface{}{
"chat_id": chatID,
"text": text,
}
if parseMode != "" {
body["parse_mode"] = parseMode
}
if replyTo != "" {
body["reply_to_message_id"] = replyTo
}
data, _ := json.Marshal(body)
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", a.token)
resp, err := a.client.Post(url, "application/json", bytes.NewReader(data))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// SendChatAction sends a "typing..." indicator via Telegram Bot API.
func (a *Adapter) SendChatAction(chatID int64, action string) error {
body, _ := json.Marshal(map[string]interface{}{
"chat_id": chatID,
"action": action, // "typing", "upload_photo", etc.
})
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendChatAction", a.token)
resp, err := a.client.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}