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() }