feat(channels/telegram): Telegram 渠道插件(Bot API 长轮询/Webhook + file_id 媒体 + 主动推送强)
This commit is contained in:
1 parent
0b036645bb
commit
f9f5a78a02
12 files changed
+1983
No files matched your search
@@ -0,0 +1,223 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
)
|
||||
|
||||
// telegramUpdate is the top-level object Telegram sends for every event.
|
||||
type telegramUpdate struct {
|
||||
UpdateID int64 `json:"update_id"`
|
||||
Message *telegramMessage `json:"message"`
|
||||
EditedMessage *telegramMessage `json:"edited_message"`
|
||||
ChannelPost *telegramMessage `json:"channel_post"`
|
||||
EditedChannelPost *telegramMessage `json:"edited_channel_post"`
|
||||
}
|
||||
|
||||
// message returns the concrete chat message carried by the update, or nil.
|
||||
func (u telegramUpdate) message() *telegramMessage {
|
||||
switch {
|
||||
case u.Message != nil:
|
||||
return u.Message
|
||||
case u.EditedMessage != nil:
|
||||
return u.EditedMessage
|
||||
case u.ChannelPost != nil:
|
||||
return u.ChannelPost
|
||||
case u.EditedChannelPost != nil:
|
||||
return u.EditedChannelPost
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// telegramMessage is a chat message as delivered by the Bot API.
|
||||
type telegramMessage struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
From *telegramUser `json:"from"`
|
||||
Chat telegramChat `json:"chat"`
|
||||
Date int64 `json:"date"`
|
||||
Text string `json:"text"`
|
||||
Caption string `json:"caption"`
|
||||
Photo []telegramPhotoSize `json:"photo"`
|
||||
Voice *telegramAudio `json:"voice"`
|
||||
Audio *telegramAudio `json:"audio"`
|
||||
Document *telegramDocument `json:"document"`
|
||||
Video *telegramVideo `json:"video"`
|
||||
ReplyToMessage *telegramMessage `json:"reply_to_message"`
|
||||
MessageThreadID int64 `json:"message_thread_id"`
|
||||
}
|
||||
|
||||
// telegramUser is the sender of a message.
|
||||
type telegramUser struct {
|
||||
ID int64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// telegramChat is the conversation a message belongs to.
|
||||
type telegramChat struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"` // private | group | supergroup | channel
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// telegramPhotoSize is one entry of a photo attachment.
|
||||
type telegramPhotoSize struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
}
|
||||
|
||||
// telegramAudio describes a voice or audio attachment.
|
||||
type telegramAudio struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MIMEType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
// telegramDocument describes a document attachment.
|
||||
type telegramDocument struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
FileName string `json:"file_name"`
|
||||
MIMEType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
// telegramVideo describes a video attachment.
|
||||
type telegramVideo struct {
|
||||
FileID string `json:"file_id"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MIMEType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
// toUnifiedMessage converts an update to the normalized model. It returns ok
|
||||
// false for updates that carry no deliverable chat message.
|
||||
func (a *Adapter) toUnifiedMessage(update telegramUpdate) (adapter.UnifiedMessage, bool) {
|
||||
m := update.message()
|
||||
if m == nil {
|
||||
return adapter.UnifiedMessage{}, false
|
||||
}
|
||||
msg := adapter.UnifiedMessage{
|
||||
Platform: a.Name(),
|
||||
ChannelID: strconv.FormatInt(m.Chat.ID, 10),
|
||||
ChannelType: chatType(m.Chat.Type),
|
||||
MessageID: strconv.FormatInt(m.MessageID, 10),
|
||||
Timestamp: time.Unix(m.Date, 0),
|
||||
}
|
||||
|
||||
if m.Chat.Type == "group" || m.Chat.Type == "supergroup" || m.Chat.Type == "channel" {
|
||||
msg.GroupID = strconv.FormatInt(m.Chat.ID, 10)
|
||||
msg.GroupName = m.Chat.Title
|
||||
}
|
||||
if m.MessageThreadID != 0 {
|
||||
msg.ThreadID = strconv.FormatInt(m.MessageThreadID, 10)
|
||||
}
|
||||
|
||||
content, contentType, media := extractPayload(m)
|
||||
msg.Content = content
|
||||
msg.ContentType = contentType
|
||||
msg.Media = media
|
||||
|
||||
if m.From != nil {
|
||||
msg.SenderID = strconv.FormatInt(m.From.ID, 10)
|
||||
msg.SenderName = displayName(m.From)
|
||||
msg.Sender = adapter.Actor{
|
||||
Role: adapter.RoleUnknown,
|
||||
UserID: msg.SenderID,
|
||||
DisplayName: msg.SenderName,
|
||||
GroupID: msg.GroupID,
|
||||
GroupName: msg.GroupName,
|
||||
}.Resolve()
|
||||
}
|
||||
|
||||
if m.ReplyToMessage != nil {
|
||||
msg.ReplyToID = strconv.FormatInt(m.ReplyToMessage.MessageID, 10)
|
||||
msg.ReplyToText = m.ReplyToMessage.Text
|
||||
if msg.ReplyToText == "" {
|
||||
msg.ReplyToText = m.ReplyToMessage.Caption
|
||||
}
|
||||
}
|
||||
|
||||
a.recordChat(m.Chat, m.From)
|
||||
return msg, true
|
||||
}
|
||||
|
||||
// extractPayload picks the primary content and attachment list from a message.
|
||||
func extractPayload(m *telegramMessage) (string, adapter.ContentType, []adapter.Media) {
|
||||
media := make([]adapter.Media, 0, 2)
|
||||
content := m.Caption
|
||||
if m.Text != "" {
|
||||
content = m.Text
|
||||
}
|
||||
ct := adapter.ContentText
|
||||
|
||||
if len(m.Photo) > 0 {
|
||||
p := m.Photo[len(m.Photo)-1]
|
||||
media = append(media, adapter.Media{Kind: adapter.MediaImage, URL: p.FileID, Size: p.FileSize, MIMEType: "image/jpeg"})
|
||||
ct = adapter.ContentImage
|
||||
}
|
||||
if m.Voice != nil {
|
||||
media = append(media, adapter.Media{Kind: adapter.MediaVoice, URL: m.Voice.FileID, Size: m.Voice.FileSize, MIMEType: m.Voice.MIMEType})
|
||||
ct = adapter.ContentVoice
|
||||
} else if m.Audio != nil {
|
||||
media = append(media, adapter.Media{Kind: adapter.MediaVoice, URL: m.Audio.FileID, Size: m.Audio.FileSize, MIMEType: m.Audio.MIMEType})
|
||||
ct = adapter.ContentVoice
|
||||
}
|
||||
if m.Document != nil {
|
||||
media = append(media, adapter.Media{Kind: adapter.MediaFile, URL: m.Document.FileID, Size: m.Document.FileSize, Name: m.Document.FileName, MIMEType: m.Document.MIMEType})
|
||||
ct = adapter.ContentFile
|
||||
}
|
||||
if m.Video != nil {
|
||||
media = append(media, adapter.Media{Kind: adapter.MediaVideo, URL: m.Video.FileID, Size: m.Video.FileSize, MIMEType: m.Video.MIMEType})
|
||||
ct = adapter.ContentFile
|
||||
}
|
||||
|
||||
return content, ct, media
|
||||
}
|
||||
|
||||
// chatType maps a Telegram chat type to the adapter's ChannelType.
|
||||
func chatType(t string) adapter.ChannelType {
|
||||
if t == "group" || t == "supergroup" || t == "channel" {
|
||||
return adapter.ChannelGroup
|
||||
}
|
||||
return adapter.ChannelDirect
|
||||
}
|
||||
|
||||
// displayName renders a sender's best human-readable name without leaking
|
||||
// anything sensitive.
|
||||
func displayName(u *telegramUser) string {
|
||||
if u.FirstName != "" && u.LastName != "" {
|
||||
return u.FirstName + " " + u.LastName
|
||||
}
|
||||
if u.FirstName != "" {
|
||||
return u.FirstName
|
||||
}
|
||||
if u.Username != "" {
|
||||
return u.Username
|
||||
}
|
||||
return strconv.FormatInt(u.ID, 10)
|
||||
}
|
||||
|
||||
// recordChat caches a conversation so ListChannels can report it.
|
||||
func (a *Adapter) recordChat(c telegramChat, from *telegramUser) {
|
||||
info := adapter.ChannelInfo{
|
||||
ChannelID: strconv.FormatInt(c.ID, 10),
|
||||
ChannelType: chatType(c.Type),
|
||||
ChannelName: c.Title,
|
||||
}
|
||||
if info.ChannelType == adapter.ChannelGroup {
|
||||
info.GroupID = info.ChannelID
|
||||
info.GroupName = c.Title
|
||||
} else if from != nil {
|
||||
info.ChannelName = displayName(from)
|
||||
}
|
||||
if info.ChannelName == "" {
|
||||
info.ChannelName = info.ChannelID
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.chats[info.ChannelID] = info
|
||||
a.mu.Unlock()
|
||||
}
|
||||
Reference in new issue
Block a user