direct media: implement direct download for photos

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
Sumner Evans
2024-06-18 18:08:20 -06:00
parent d0626e670c
commit 7963e52405
10 changed files with 415 additions and 170 deletions
+16 -15
View File
@@ -17,6 +17,7 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
"go.mau.fi/mautrix-telegram/pkg/connector/msgconv"
)
@@ -71,7 +72,7 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge
Logger: zaplog,
UpdateHandler: updatesManager,
})
client.msgConv = msgconv.NewMessageConverter(client.client)
client.msgConv = msgconv.NewMessageConverter(client.client, tc.useDirectMedia)
client.clientCancel, err = connectTelegramClient(ctx, client.client)
go func() {
err = updatesManager.Run(ctx, client.client.API(), loginID, updates.AuthOptions{})
@@ -127,21 +128,21 @@ func (t *TelegramClient) onUpdateNewMessage(ctx context.Context, e tg.Entities,
var sender bridgev2.EventSender
if msg.Out {
sender.IsFromMe = true
sender.SenderLogin = makeUserLoginID(t.loginID)
sender.Sender = makeUserID(t.loginID)
sender.SenderLogin = ids.MakeUserLoginID(t.loginID)
sender.Sender = ids.MakeUserID(t.loginID)
} else if msg.FromID != nil {
switch from := msg.FromID.(type) {
case *tg.PeerUser:
sender.SenderLogin = makeUserLoginID(from.UserID)
sender.Sender = makeUserID(from.UserID)
sender.SenderLogin = ids.MakeUserLoginID(from.UserID)
sender.Sender = ids.MakeUserID(from.UserID)
default:
fmt.Printf("%+v\n", msg.FromID)
fmt.Printf("%T\n", msg.FromID)
panic("unimplemented FromID")
}
} else if peer, ok := msg.PeerID.(*tg.PeerUser); ok {
sender.SenderLogin = makeUserLoginID(peer.UserID)
sender.Sender = makeUserID(peer.UserID)
sender.SenderLogin = ids.MakeUserLoginID(peer.UserID)
sender.Sender = ids.MakeUserID(peer.UserID)
} else {
panic("not from anyone")
}
@@ -155,9 +156,9 @@ func (t *TelegramClient) onUpdateNewMessage(ctx context.Context, e tg.Entities,
Str("sender_login", string(sender.SenderLogin)).
Bool("is_from_me", sender.IsFromMe)
},
ID: makeMessageID(msg.ID),
ID: ids.MakeMessageID(msg.ID),
Sender: sender,
PortalKey: makePortalID(msg.PeerID),
PortalKey: ids.MakePortalID(msg.PeerID),
Data: msg,
CreatePortal: true,
ConvertMessageFunc: t.msgConv.ToMatrix,
@@ -186,7 +187,7 @@ func getFullName(user *tg.User) string {
func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.PortalInfo, error) {
fmt.Printf("%+v\n", portal)
peerType, id, err := parsePortalID(portal.ID)
peerType, id, err := ids.ParsePortalID(portal.ID)
if err != nil {
return nil, err
}
@@ -195,7 +196,7 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta
var isSpace, isDM bool
switch peerType {
case peerTypeUser:
case ids.PeerTypeUser:
users, err := t.client.API().UsersGetUsers(ctx, []tg.InputUserClass{&tg.InputUser{UserID: id}})
if err != nil {
return nil, err
@@ -207,10 +208,10 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta
return nil, fmt.Errorf("returned user is not *tg.User")
} else {
name = getFullName(user) // TODO gate this behind a config?
members = []networkid.UserID{makeUserID(id), makeUserID(t.loginID)}
members = []networkid.UserID{ids.MakeUserID(id), ids.MakeUserID(t.loginID)}
isDM = true
}
case peerTypeChat:
case ids.PeerTypeChat:
// TODO get name of chat
chat, err := t.client.API().MessagesGetFullChat(ctx, id)
if err != nil {
@@ -220,7 +221,7 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta
return nil, fmt.Errorf("no users found in chat %d", id)
}
for _, user := range chat.Users {
members = append(members, makeUserID(user.GetID()))
members = append(members, ids.MakeUserID(user.GetID()))
}
default:
fmt.Printf("%s %d\n", peerType, id)
@@ -240,7 +241,7 @@ func (t *TelegramClient) GetChatInfo(ctx context.Context, portal *bridgev2.Porta
}
func (t *TelegramClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
id, err := parseUserID(ghost.ID)
id, err := ids.ParseUserID(ghost.ID)
if err != nil {
return nil, err
}
+2 -1
View File
@@ -37,7 +37,8 @@ type TelegramConnector struct {
Bridge *bridgev2.Bridge
Config *TelegramConfig
store *store.Container
store *store.Container
useDirectMedia bool
}
var _ bridgev2.NetworkConnector = (*TelegramConnector)(nil)
+105
View File
@@ -0,0 +1,105 @@
package connector
import (
"bytes"
"context"
"fmt"
"io"
"github.com/gotd/td/tg"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/mediaproxy"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
conmedia "go.mau.fi/mautrix-telegram/pkg/connector/media"
)
var _ bridgev2.DirectMediableNetwork = (*TelegramConnector)(nil)
type getMessages interface {
GetMessages() []tg.MessageClass
}
func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.MediaID) (mediaproxy.GetMediaResponse, error) {
info, err := ids.ParseDirectMediaInfo(mediaID)
if err != nil {
return nil, err
}
logins, err := tc.Bridge.GetUserLoginsInPortal(ctx, info.PeerType.AsPortalKey(info.ChatID))
if err != nil {
return nil, err
} else if len(logins) == 0 {
return nil, fmt.Errorf("no user logins in the portal (%s %d)", info.PeerType, info.ChatID)
}
client := logins[0].Client.(*TelegramClient)
var messages tg.MessagesMessagesClass
switch info.PeerType {
case ids.PeerTypeUser, ids.PeerTypeChat:
messages, err = client.client.API().MessagesGetMessages(ctx, []tg.InputMessageClass{
&tg.InputMessageID{ID: int(info.MessageID)},
})
case ids.PeerTypeChannel:
messages, err = client.client.API().ChannelsGetMessages(ctx, &tg.ChannelsGetMessagesRequest{
Channel: &tg.InputChannel{ChannelID: info.ChatID},
ID: []tg.InputMessageClass{
&tg.InputMessageID{ID: int(info.MessageID)},
},
})
default:
return nil, fmt.Errorf("unknown peer type %s", info.PeerType)
}
if err != nil {
return nil, err
}
var media tg.MessageMediaClass
if m, ok := messages.(getMessages); !ok {
return nil, fmt.Errorf("unknown message type")
} else {
for _, message := range m.GetMessages() {
if msg, ok := message.(*tg.Message); ok && msg.ID == int(info.MessageID) {
media = msg.Media
break
}
}
}
switch media := media.(type) {
case *tg.MessageMediaPhoto:
data, mimeType, err := conmedia.DownloadPhoto(ctx, client.client.API(), media)
if err != nil {
return nil, err
}
return &mediaproxy.GetMediaResponseData{
Reader: io.NopCloser(bytes.NewBuffer(data)),
ContentType: mimeType,
ContentLength: int64(len(data)),
}, nil
// TODO all of these
// case *tg.MessageMediaGeo: // messageMediaGeo#56e0d474
// case *tg.MessageMediaContact: // messageMediaContact#70322949
// case *tg.MessageMediaUnsupported: // messageMediaUnsupported#9f84f49e
// case *tg.MessageMediaDocument: // messageMediaDocument#4cf4d72d
// case *tg.MessageMediaWebPage: // messageMediaWebPage#ddf10c3b
// case *tg.MessageMediaVenue: // messageMediaVenue#2ec0533f
// case *tg.MessageMediaGame: // messageMediaGame#fdb19008
// case *tg.MessageMediaInvoice: // messageMediaInvoice#f6a548d3
// case *tg.MessageMediaGeoLive: // messageMediaGeoLive#b940c666
// case *tg.MessageMediaPoll: // messageMediaPoll#4bd6e798
// case *tg.MessageMediaDice: // messageMediaDice#3f7ee58b
// case *tg.MessageMediaStory: // messageMediaStory#68cb6283
// case *tg.MessageMediaGiveaway: // messageMediaGiveaway#daad85b0
// case *tg.MessageMediaGiveawayResults: // messageMediaGiveawayResults#c6991068
default:
return nil, fmt.Errorf("unhandled media type %T", media)
}
}
func (tg *TelegramConnector) SetUseDirectMedia() {
tg.useDirectMedia = true
}
-71
View File
@@ -1,71 +0,0 @@
package connector
import (
"fmt"
"strconv"
"strings"
"github.com/gotd/td/tg"
"maunium.net/go/mautrix/bridgev2/networkid"
)
func makeUserID(userID int64) networkid.UserID {
return networkid.UserID(strconv.FormatInt(userID, 10))
}
func parseUserID(userID networkid.UserID) (int64, error) {
return strconv.ParseInt(string(userID), 10, 64)
}
func makeUserLoginID(userID int64) networkid.UserLoginID {
return networkid.UserLoginID(strconv.FormatInt(userID, 10))
}
func makeMessageID(messageID int) networkid.MessageID {
return networkid.MessageID(strconv.Itoa(messageID))
}
type peerType string
const (
peerTypeUser peerType = "user"
peerTypeChat peerType = "chat"
peerTypeChannel peerType = "channel"
)
func makePortalID(peer tg.PeerClass) networkid.PortalKey {
switch v := peer.(type) {
case *tg.PeerUser:
return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeUser, v.UserID))}
case *tg.PeerChat:
return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeChat, v.ChatID))}
case *tg.PeerChannel:
return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", peerTypeChannel, v.ChannelID))}
default:
panic(fmt.Errorf("unknown peer class type %T", v))
}
}
func parsePortalID(portalID networkid.PortalID) (pt peerType, id int64, err error) {
parts := strings.Split(string(portalID), ":")
pt = peerType(parts[0])
id, err = strconv.ParseInt(parts[1], 10, 64)
return
}
func inputPeerForPortalID(portalID networkid.PortalID) (tg.InputPeerClass, error) {
peerType, id, err := parsePortalID(portalID)
if err != nil {
return nil, err
}
switch peerType {
case peerTypeUser:
return &tg.InputPeerUser{UserID: id}, nil
case peerTypeChat:
return &tg.InputPeerChat{ChatID: id}, nil
case peerTypeChannel:
return &tg.InputPeerChannel{ChannelID: id}, nil
default:
panic("invalid peer type")
}
}
+101
View File
@@ -0,0 +1,101 @@
package ids
import (
"fmt"
"strconv"
"strings"
"github.com/gotd/td/tg"
"maunium.net/go/mautrix/bridgev2/networkid"
)
func MakeUserID(userID int64) networkid.UserID {
return networkid.UserID(strconv.FormatInt(userID, 10))
}
func ParseUserID(userID networkid.UserID) (int64, error) {
return strconv.ParseInt(string(userID), 10, 64)
}
func MakeUserLoginID(userID int64) networkid.UserLoginID {
return networkid.UserLoginID(strconv.FormatInt(userID, 10))
}
func MakeMessageID(messageID int) networkid.MessageID {
return networkid.MessageID(strconv.Itoa(messageID))
}
type PeerType string
const (
PeerTypeUser PeerType = "user"
PeerTypeChat PeerType = "chat"
PeerTypeChannel PeerType = "channel"
)
func PeerTypeFromByte(pt byte) (PeerType, error) {
switch pt {
case 0x01:
return PeerTypeUser, nil
case 0x02:
return PeerTypeChat, nil
case 0x03:
return PeerTypeChannel, nil
default:
return "", fmt.Errorf("unknown peer type %d", pt)
}
}
func (pt PeerType) AsByte() byte {
switch pt {
case PeerTypeUser:
return 0x01
case PeerTypeChat:
return 0x02
case PeerTypeChannel:
return 0x03
default:
panic(fmt.Errorf("unknown peer type %s", pt))
}
}
func (pt PeerType) AsPortalKey(chatID int64) networkid.PortalKey {
return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", pt, chatID))}
}
func MakePortalID(peer tg.PeerClass) networkid.PortalKey {
switch v := peer.(type) {
case *tg.PeerUser:
return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", PeerTypeUser, v.UserID))}
case *tg.PeerChat:
return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", PeerTypeChat, v.ChatID))}
case *tg.PeerChannel:
return networkid.PortalKey{ID: networkid.PortalID(fmt.Sprintf("%s:%d", PeerTypeChannel, v.ChannelID))}
default:
panic(fmt.Errorf("unknown peer class type %T", v))
}
}
func ParsePortalID(portalID networkid.PortalID) (pt PeerType, id int64, err error) {
parts := strings.Split(string(portalID), ":")
pt = PeerType(parts[0])
id, err = strconv.ParseInt(parts[1], 10, 64)
return
}
func InputPeerForPortalID(portalID networkid.PortalID) (tg.InputPeerClass, error) {
peerType, id, err := ParsePortalID(portalID)
if err != nil {
return nil, err
}
switch peerType {
case PeerTypeUser:
return &tg.InputPeerUser{UserID: id}, nil
case PeerTypeChat:
return &tg.InputPeerChat{ChatID: id}, nil
case PeerTypeChannel:
return &tg.InputPeerChannel{ChannelID: id}, nil
default:
panic("invalid peer type")
}
}
+59
View File
@@ -0,0 +1,59 @@
package ids
import (
"encoding/binary"
"fmt"
"maunium.net/go/mautrix/bridgev2/networkid"
)
// DirectMediaInfo is the information that is encoded in the media ID when
// using direct media.
//
// The format of the media ID is as follows (each character represents a single
// byte, |'s added for clarity):
//
// v|p|CCCCCCCC|TTTTTTTT
//
// v (int8) = binary encoding format version. Should be 0.
// p (byte) = the peer type of the Telegram chat ID
// CCCCCCCC (int64) = the Telegram chat ID (big endian)
// TTTTTTTT (int64) = the Telegram message ID (big endian)
type DirectMediaInfo struct {
PeerType PeerType
ChatID int64
MessageID int64
}
func (m DirectMediaInfo) AsMediaID() (networkid.MediaID, error) {
mediaID := []byte{
0x00, // Version
m.PeerType.AsByte(), // Peer Type
}
mediaID = binary.BigEndian.AppendUint64(mediaID, uint64(m.ChatID)) // Telegram Chat ID
mediaID = binary.BigEndian.AppendUint64(mediaID, uint64(m.MessageID)) // Telegram Message ID
return mediaID, nil
}
func ParseDirectMediaInfo(mediaID networkid.MediaID) (info DirectMediaInfo, err error) {
if len(mediaID) == 0 {
err = fmt.Errorf("empty media ID")
return
}
if mediaID[0] != 0x00 {
err = fmt.Errorf("invalid version %d", mediaID[0])
return
}
if len(mediaID) != 18 {
err = fmt.Errorf("invalid media ID")
return
}
fmt.Printf("%v\n", mediaID)
info.PeerType, err = PeerTypeFromByte(mediaID[1])
if err != nil {
return
}
info.ChatID = int64(binary.BigEndian.Uint64(mediaID[2:]))
info.MessageID = int64(binary.BigEndian.Uint64(mediaID[10:]))
return
}
+3 -1
View File
@@ -30,6 +30,8 @@ import (
"go.uber.org/zap"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
)
const LoginFlowIDPhone = "phone"
@@ -187,7 +189,7 @@ func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.Au
}
p.clientCancel()
userLoginID := makeUserLoginID(authorization.User.GetID())
userLoginID := ids.MakeUserLoginID(authorization.User.GetID())
ul, err := p.user.NewLogin(ctx, &database.UserLogin{
ID: userLoginID,
Metadata: database.UserLoginMetadata{
+83
View File
@@ -0,0 +1,83 @@
package media
import (
"bytes"
"context"
"fmt"
"net/http"
"github.com/gotd/td/telegram/downloader"
"github.com/gotd/td/tg"
)
func getLargestPhotoSize(sizes []tg.PhotoSizeClass) (largest tg.PhotoSizeClass) {
var maxSize int
for _, s := range sizes {
var currentSize int
switch size := s.(type) {
case *tg.PhotoSize:
currentSize = size.GetSize()
case *tg.PhotoCachedSize:
currentSize = max(size.GetW(), size.GetH())
case *tg.PhotoSizeProgressive:
currentSize = max(size.GetW(), size.GetH())
case *tg.PhotoPathSize:
currentSize = len(size.GetBytes())
case *tg.PhotoStrippedSize:
currentSize = len(size.GetBytes())
}
if currentSize > maxSize {
maxSize = currentSize
largest = s
}
}
return
}
func DownloadPhoto(ctx context.Context, client downloader.Client, media *tg.MessageMediaPhoto) (data []byte, mimeType string, err error) {
p, ok := media.GetPhoto()
if !ok {
return nil, "", fmt.Errorf("photo message sent without a photo")
}
photo, ok := p.(*tg.Photo)
if !ok {
return nil, "", fmt.Errorf("unrecognized photo type %T", p)
}
largest := getLargestPhotoSize(photo.GetSizes())
file := tg.InputPhotoFileLocation{
ID: photo.GetID(),
AccessHash: photo.GetAccessHash(),
FileReference: photo.GetFileReference(),
ThumbSize: largest.GetType(),
}
// TODO convert to streaming
var buf bytes.Buffer
storageFileTypeClass, err := downloader.NewDownloader().Download(client, &file).Stream(ctx, &buf)
if err != nil {
return nil, "", err
}
switch storageFileTypeClass.(type) {
case *tg.StorageFileJpeg:
mimeType = "image/jpeg"
case *tg.StorageFileGif:
mimeType = "image/gif"
case *tg.StorageFilePng:
mimeType = "image/png"
case *tg.StorageFilePdf:
mimeType = "application/pdf"
case *tg.StorageFileMp3:
mimeType = "audio/mp3"
case *tg.StorageFileMov:
mimeType = "video/quicktime"
case *tg.StorageFileMp4:
mimeType = "video/mp4"
case *tg.StorageFileWebp:
mimeType = "image/webp"
default:
mimeType = http.DetectContentType(buf.Bytes())
}
return buf.Bytes(), mimeType, nil
}
+4 -2
View File
@@ -4,8 +4,10 @@ import "github.com/gotd/td/telegram"
type MessageConverter struct {
client *telegram.Client
useDirectMedia bool
}
func NewMessageConverter(client *telegram.Client) *MessageConverter {
return &MessageConverter{client: client}
func NewMessageConverter(client *telegram.Client, useDirectMedia bool) *MessageConverter {
return &MessageConverter{client: client, useDirectMedia: useDirectMedia}
}
+42 -80
View File
@@ -1,18 +1,19 @@
package msgconv
import (
"bytes"
"context"
"fmt"
"net/http"
"github.com/gotd/td/telegram/downloader"
"github.com/gotd/td/tg"
"github.com/rs/zerolog"
"go.mau.fi/util/exmime"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
conmedia "go.mau.fi/mautrix-telegram/pkg/connector/media"
)
func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, msg *tg.Message) (*bridgev2.ConvertedMessage, error) {
@@ -27,64 +28,50 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta
Content: &event.MessageEventContent{MsgType: event.MsgText, Body: msg.Message},
})
}
if m, ok := msg.GetMedia(); ok {
switch media := m.(type) {
case *tg.MessageMediaPhoto:
p, ok := media.GetPhoto()
if !ok {
return nil, fmt.Errorf("photo message sent without a photo")
}
photo, ok := p.(*tg.Photo)
if !ok {
return nil, fmt.Errorf("unrecognized photo type %T", p)
}
largest := getLargestPhotoSize(photo.GetSizes())
file := tg.InputPhotoFileLocation{
ID: photo.GetID(),
AccessHash: photo.GetAccessHash(),
FileReference: photo.GetFileReference(),
ThumbSize: largest.GetType(),
}
// TODO convert to streaming directly into UploadMedia
var buf bytes.Buffer
storageFileTypeClass, err := downloader.NewDownloader().Download(mc.client.API(), &file).Stream(ctx, &buf)
if err != nil {
return nil, err
}
var mimeType string
switch storageFileTypeClass.(type) {
case *tg.StorageFileJpeg:
mimeType = "image/jpeg"
case *tg.StorageFileGif:
mimeType = "image/gif"
case *tg.StorageFilePng:
mimeType = "image/png"
case *tg.StorageFilePdf:
mimeType = "application/pdf"
case *tg.StorageFileMp3:
mimeType = "audio/mp3"
case *tg.StorageFileMov:
mimeType = "video/quicktime"
case *tg.StorageFileMp4:
mimeType = "video/mp4"
case *tg.StorageFileWebp:
mimeType = "image/webp"
default:
mimeType = http.DetectContentType(buf.Bytes())
}
var filename string
if _, ok := media.GetTTLSeconds(); ok {
// TODO set the ttl on the converted message
filename = "disappearing_image" + exmime.ExtensionFromMimetype(mimeType)
} else {
filename = "image" + exmime.ExtensionFromMimetype(mimeType)
var mxcURI id.ContentURIString
var encryptedFileInfo *event.EncryptedFileInfo
if mc.useDirectMedia {
var err error
filename = "image"
peerType, chatID, err := ids.ParsePortalID(portal.ID)
if err != nil {
return nil, err
}
mediaID, err := ids.DirectMediaInfo{
PeerType: peerType,
ChatID: chatID,
MessageID: int64(msg.ID),
}.AsMediaID()
if err != nil {
return nil, err
}
mxcURI, err = portal.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
if err != nil {
return nil, err
}
}
mxcURI, encryptedFileInfo, err := intent.UploadMedia(ctx, "", buf.Bytes(), filename, mimeType)
if err != nil {
return nil, err
if mxcURI == "" {
data, mimeType, err := conmedia.DownloadPhoto(ctx, mc.client.API(), media)
if err != nil {
return nil, err
}
if _, ok := media.GetTTLSeconds(); ok {
// TODO set the ttl on the converted message
filename = "disappearing_image" + exmime.ExtensionFromMimetype(mimeType)
} else {
filename = "image" + exmime.ExtensionFromMimetype(mimeType)
}
mxcURI, encryptedFileInfo, err = intent.UploadMedia(ctx, "", data, filename, mimeType)
if err != nil {
return nil, err
}
}
extra := map[string]any{}
@@ -128,28 +115,3 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Porta
}
return cm, nil
}
func getLargestPhotoSize(sizes []tg.PhotoSizeClass) (largest tg.PhotoSizeClass) {
var maxSize int
for _, s := range sizes {
var currentSize int
switch size := s.(type) {
case *tg.PhotoSize:
currentSize = size.GetSize()
case *tg.PhotoCachedSize:
currentSize = max(size.GetW(), size.GetH())
case *tg.PhotoSizeProgressive:
currentSize = max(size.GetW(), size.GetH())
case *tg.PhotoPathSize:
currentSize = len(size.GetBytes())
case *tg.PhotoStrippedSize:
currentSize = len(size.GetBytes())
}
if currentSize > maxSize {
maxSize = currentSize
largest = s
}
}
return
}