feat: Phase 4 多平台接入 — Platform Bridge + 6平台适配器 + 身份权限系统 (22文件, 2129行)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user