965cce7192
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
252 lines
6.8 KiB
Go
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
|
|
}
|