From a63f264804473fe53c8c9f92c92b68a4cf24f4b8 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Fri, 28 Jun 2024 10:41:28 -0600 Subject: [PATCH] reactions: support custom emojis Signed-off-by: Sumner Evans --- go.mod | 2 +- go.sum | 4 +- pkg/connector/client.go | 2 +- pkg/connector/config.go | 47 ++++++++ pkg/connector/connector.go | 32 ------ pkg/connector/directdownload.go | 2 +- pkg/connector/emojis/emojis.go | 3 +- pkg/connector/example-config.yaml | 83 +++----------- pkg/connector/matrix.go | 2 + pkg/connector/media/download.go | 6 +- pkg/connector/media/transfer.go | 122 +++++++++++++++++---- pkg/connector/msgconv/converter.go | 19 +++- pkg/connector/msgconv/tomatrix.go | 29 ++--- pkg/connector/store/telegramfile.go | 62 ++--------- pkg/connector/store/upgrades/00-latest.sql | 16 +-- pkg/connector/telegram.go | 41 ++++--- 16 files changed, 244 insertions(+), 228 deletions(-) create mode 100644 pkg/connector/config.go diff --git a/go.mod b/go.mod index 23b98720..6334645c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gotd/td v0.102.0 github.com/rs/zerolog v1.33.0 github.com/stretchr/testify v1.9.0 - go.mau.fi/util v0.5.1-0.20240702170310-bd1da3c069eb + go.mau.fi/util v0.5.1-0.20240708233020-c2f9af6fecf8 go.mau.fi/zerozap v0.1.1 go.uber.org/zap v1.27.0 maunium.net/go/mautrix v0.19.0-beta.1.0.20240706124659-b4057a26c3ed diff --git a/go.sum b/go.sum index af7ef4b7..e0c88fe4 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.mau.fi/util v0.5.1-0.20240702170310-bd1da3c069eb h1:VZPo2pvfjNj6fkFv5e9FyTYx96BLwwYNA19WYaY+KN8= -go.mau.fi/util v0.5.1-0.20240702170310-bd1da3c069eb/go.mod h1:DsJzUrJAG53lCZnnYvq9/mOyLuPScWwYhvETiTrpdP4= +go.mau.fi/util v0.5.1-0.20240708233020-c2f9af6fecf8 h1:7ntkhSR0G/dIwAPjcoOoJz+bPne0gA4PypEn8euMMx0= +go.mau.fi/util v0.5.1-0.20240708233020-c2f9af6fecf8/go.mod h1:DsJzUrJAG53lCZnnYvq9/mOyLuPScWwYhvETiTrpdP4= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= go.mau.fi/zerozap v0.1.1 h1:mxE/dW4wtkqBYOXOEEzXldk5qKB+ahsZXjoTGnvEhZQ= diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 29731a0f..1088adc9 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -119,7 +119,7 @@ func NewTelegramClient(ctx context.Context, tc *TelegramConnector, login *bridge Logger: zaplog, UpdateHandler: updatesManager, }) - client.msgConv = msgconv.NewMessageConverter(client.client, tc.Bridge.Matrix, tc.useDirectMedia) + client.msgConv = msgconv.NewMessageConverter(client.client, tc.Bridge.Matrix, tc.Store, tc.Config.AnimatedSticker, tc.useDirectMedia) client.clientCancel, err = connectTelegramClient(ctx, client.client) client.reactionMessageLocks = map[int]*sync.Mutex{} go func() { diff --git a/pkg/connector/config.go b/pkg/connector/config.go new file mode 100644 index 00000000..78ead697 --- /dev/null +++ b/pkg/connector/config.go @@ -0,0 +1,47 @@ +package connector + +import ( + _ "embed" + "fmt" + + up "go.mau.fi/util/configupgrade" + "maunium.net/go/mautrix/bridgev2" + + "go.mau.fi/mautrix-telegram/pkg/connector/media" +) + +var _ bridgev2.ConfigValidatingNetwork = (*TelegramConnector)(nil) + +type TelegramConfig struct { + AppID int `yaml:"app_id"` + AppHash string `yaml:"app_hash"` + + AnimatedSticker media.AnimatedStickerConfig `yaml:"animated_sticker"` +} + +//go:embed example-config.yaml +var ExampleConfig string + +func upgradeConfig(helper up.Helper) { + helper.Copy(up.Int, "app_id") + helper.Copy(up.Str, "app_hash") + helper.Copy(up.Str, "animated_sticker.target") + helper.Copy(up.Bool, "animated_sticker.convert_from_webm") + helper.Copy(up.Int, "animated_sticker.args.width") + helper.Copy(up.Int, "animated_sticker.args.height") + helper.Copy(up.Int, "animated_sticker.args.fps") +} + +func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up.Upgrader) { + return ExampleConfig, tg.Config, up.SimpleUpgrader(upgradeConfig) +} + +func (tg *TelegramConnector) ValidateConfig() error { + if tg.Config.AppID == 0 { + return fmt.Errorf("app_id is required") + } + if tg.Config.AppHash == "" { + return fmt.Errorf("app_hash is required") + } + return nil +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 9275105c..37c0ba0d 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -18,21 +18,13 @@ package connector import ( "context" - _ "embed" - "fmt" - up "go.mau.fi/util/configupgrade" "go.mau.fi/util/dbutil" "maunium.net/go/mautrix/bridgev2" "go.mau.fi/mautrix-telegram/pkg/connector/store" ) -type TelegramConfig struct { - AppID int `yaml:"app_id"` - AppHash string `yaml:"app_hash"` -} - type TelegramConnector struct { Bridge *bridgev2.Bridge Config *TelegramConfig @@ -42,7 +34,6 @@ type TelegramConnector struct { } var _ bridgev2.NetworkConnector = (*TelegramConnector)(nil) -var _ bridgev2.ConfigValidatingNetwork = (*TelegramConnector)(nil) // var _ bridgev2.MaxFileSizeingNetwork = (*TelegramConnector)(nil) @@ -53,7 +44,6 @@ func NewConnector() *TelegramConnector { } func (tg *TelegramConnector) Init(bridge *bridgev2.Bridge) { - // TODO tg.Store = store.NewStore(bridge.DB.Database, dbutil.ZeroLogger(bridge.Log.With().Str("db_section", "telegram").Logger())) tg.Bridge = bridge } @@ -67,28 +57,6 @@ func (tc *TelegramConnector) LoadUserLogin(ctx context.Context, login *bridgev2. return } -//go:embed example-config.yaml -var ExampleConfig string - -func upgradeConfig(helper up.Helper) { - helper.Copy(up.Int, "app_id") - helper.Copy(up.Str, "app_hash") -} - -func (tg *TelegramConnector) GetConfig() (example string, data any, upgrader up.Upgrader) { - return ExampleConfig, tg.Config, up.SimpleUpgrader(upgradeConfig) -} - -func (tg *TelegramConnector) ValidateConfig() error { - if tg.Config.AppID == 0 { - return fmt.Errorf("app_id is required") - } - if tg.Config.AppHash == "" { - return fmt.Errorf("app_hash is required") - } - return nil -} - // TODO // func (tg *TelegramConnector) SetMaxFileSize(maxSize int64) { // } diff --git a/pkg/connector/directdownload.go b/pkg/connector/directdownload.go index 15e54409..13a35a4f 100644 --- a/pkg/connector/directdownload.go +++ b/pkg/connector/directdownload.go @@ -91,8 +91,8 @@ func (tc *TelegramConnector) Download(ctx context.Context, mediaID networkid.Med return nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document) } - // Download the thumbnail for this media rather than the media itself. if info.Thumbnail { + // Download the thumbnail for this media rather than the media itself. _, _, largestThumbnail := media.GetLargestPhotoSize(document.Thumbs) data, mimeType, err = media.DownloadFileLocation(ctx, client.client.API(), &tg.InputDocumentFileLocation{ ID: document.GetID(), diff --git a/pkg/connector/emojis/emojis.go b/pkg/connector/emojis/emojis.go index 5736bc8c..cc54ae65 100644 --- a/pkg/connector/emojis/emojis.go +++ b/pkg/connector/emojis/emojis.go @@ -31,8 +31,7 @@ func ConvertKnownEmojis(emojiIDs []int64) (result map[networkid.EmojiID]string, result = map[networkid.EmojiID]string{} for _, e := range emojiIDs { if v, ok := reverseUnicodemojiPack[e]; ok { - emojiID := ids.MakeEmojiIDFromDocumentID(e) - result[emojiID] = v + result[ids.MakeEmojiIDFromDocumentID(e)] = v } else { remaining = append(remaining, e) } diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index b5c9a3bc..e432cb39 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -1,72 +1,19 @@ # Get your own API keys at https://my.telegram.org/apps app_id: 12345 app_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz -# (Optional) Create your own bot at https://t.me/BotFather -bot_token: disabled -# Should the bridge request missed updates from Telegram when restarting? -catch_up: true -# Should incoming updates be handled sequentially to make sure order is preserved on Matrix? -sequential_updates: true -exit_on_update_error: false - -# Telethon connection options. -connection: - # The timeout in seconds to be used when connecting. - timeout: 120 - # How many times the reconnection should retry, either on the initial connection or when - # Telegram disconnects us. May be set to a negative or null value for infinite retries, but - # this is not recommended, since the program can get stuck in an infinite loop. - retries: 5 - # The delay in seconds to sleep between automatic reconnections. - retry_delay: 1 - # The threshold below which the library should automatically sleep on flood wait errors - # (inclusive). For instance, if a FloodWaitError for 17s occurs and flood_sleep_threshold - # is 20s, the library will sleep automatically. If the error was for 21s, it would raise - # the error instead. Values larger than a day (86400) will be changed to a day. - flood_sleep_threshold: 60 - # How many times a request should be retried. Request are retried when Telegram is having - # internal issues, when there is a FloodWaitError less than flood_sleep_threshold, or when - # there's a migrate error. May take a negative or null value for infinite retries, but this - # is not recommended, since some requests can always trigger a call fail (such as searching - # for messages). - request_retries: 5 - # Use IPv6 for Telethon connection - use_ipv6: false - -# Device info sent to Telegram. -device_info: - # "auto" = OS name+version. - device_model: mautrix-telegram - # "auto" = Telethon version. - system_version: auto - # "auto" = mautrix-telegram version. - app_version: auto - lang_code: en - system_lang_code: en - -# Custom server to connect to. -server: - # Set to true to use these server settings. If false, will automatically - # use production server assigned by Telegram. Set to false in production. - enabled: false - # The DC ID to connect to. - dc: 2 - # The IP to connect to. - ip: 149.154.167.40 - # The port to connect to. 443 may not work, 80 is better and both are equally secure. - port: 80 - -# Telethon proxy configuration. -# You must install PySocks from pip for proxies to work. -proxy: - # Allowed types: disabled, socks4, socks5, http, mtproxy - type: disabled - # Proxy IP address and port. - address: 127.0.0.1 - port: 1080 - # Whether or not to perform DNS resolving remotely. Only for socks/http proxies. - rdns: true - # Proxy authentication (optional). Put MTProxy secret in password field. - username: "" - password: "" +animated_sticker: + # Format to which animated stickers should be converted. + # disable - No conversion, send as-is (gzipped lottie) + # png - converts to non-animated png (fastest), + # gif - converts to animated gif + # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support + # webp - converts to animated webp, requires ffmpeg executable with webp codec/container support + target: gif + # Should video stickers be converted to the specified format as well? + convert_from_webm: false + # Arguments for converter. All converters take width and height. + args: + width: 256 + height: 256 + fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended) diff --git a/pkg/connector/matrix.go b/pkg/connector/matrix.go index f8cb6bff..a775a6e5 100644 --- a/pkg/connector/matrix.go +++ b/pkg/connector/matrix.go @@ -193,6 +193,8 @@ func (t *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *bridg if strings.HasPrefix(msg.Content.RelatesTo.Key, "mxc://") { if file, err := t.main.Store.TelegramFile.GetByMXC(ctx, msg.Content.RelatesTo.Key); err != nil { return resp, err + } else if file == nil { + return resp, fmt.Errorf("reaction MXC URI %s does not correspond with any known Telegram files", msg.Content.RelatesTo.Key) } else if documentID, err := strconv.ParseInt(string(file.LocationID), 10, 64); err != nil { return resp, err } else { diff --git a/pkg/connector/media/download.go b/pkg/connector/media/download.go index 1425a61d..1a2a8541 100644 --- a/pkg/connector/media/download.go +++ b/pkg/connector/media/download.go @@ -9,10 +9,10 @@ import ( "github.com/gotd/td/tg" ) -func DownloadFileLocation(ctx context.Context, client downloader.Client, file tg.InputFileLocationClass) (data []byte, mimeType string, err error) { - // TODO convert to streaming? stream to file? +func DownloadFileLocation(ctx context.Context, client downloader.Client, loc tg.InputFileLocationClass) (data []byte, mimeType string, err error) { + // TODO convert entire function to streaming? Maybe at least stream to file? var buf bytes.Buffer - storageFileTypeClass, err := downloader.NewDownloader().Download(client, file).Stream(ctx, &buf) + storageFileTypeClass, err := downloader.NewDownloader().Download(client, loc).Stream(ctx, &buf) if err != nil { return nil, "", err } diff --git a/pkg/connector/media/transfer.go b/pkg/connector/media/transfer.go index b15e42c4..a35d4fa3 100644 --- a/pkg/connector/media/transfer.go +++ b/pkg/connector/media/transfer.go @@ -9,37 +9,119 @@ import ( "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" + + "go.mau.fi/util/lottie" + + "go.mau.fi/mautrix-telegram/pkg/connector/store" ) -// LocationToID converts a Telegram [tg.Document], +// getLocationID converts a Telegram [tg.Document], // [tg.InputDocumentFileLocation], [tg.InputPeerPhotoFileLocation], -// [tg.InputFileLocation], or [tg.InputPhotoFileLocation] into a key for use in -// the telegram_file table. -func LocationToID(location any) (id string) { - switch location := location.(type) { +// [tg.InputFileLocation], or [tg.InputPhotoFileLocation] into a [LocationID] +// for use in the telegram_file table. +func getLocationID(loc any) (locID store.TelegramFileLocationID) { + var id string + switch location := loc.(type) { case *tg.Document: - return fmt.Sprintf("%d", location.ID) + id = fmt.Sprintf("%d", location.ID) case *tg.InputDocumentFileLocation: - return fmt.Sprintf("%d-%s", location.ID, location.ThumbSize) + id = fmt.Sprintf("%d-%s", location.ID, location.ThumbSize) case *tg.InputPhotoFileLocation: - return fmt.Sprintf("%d-%s", location.ID, location.ThumbSize) + id = fmt.Sprintf("%d-%s", location.ID, location.ThumbSize) case *tg.InputFileLocation: - return fmt.Sprintf("%d-%d", location.VolumeID, location.LocalID) + id = fmt.Sprintf("%d-%d", location.VolumeID, location.LocalID) case *tg.InputPeerPhotoFileLocation: - return fmt.Sprintf("%d", location.PhotoID) + id = fmt.Sprintf("%d", location.PhotoID) default: panic(fmt.Errorf("unknown location type %T", location)) } + return store.TelegramFileLocationID(id) } -func TransferToMatrix(ctx context.Context, roomID id.RoomID, client downloader.Client, intent bridgev2.MatrixAPI, file tg.InputFileLocationClass, filenameOpt ...string) (id.ContentURIString, *event.EncryptedFileInfo, error) { - data, mimeType, err := DownloadFileLocation(ctx, client, file) - if err != nil { - return "", nil, fmt.Errorf("downloading file failed: %w", err) - } - var filename string - if len(filenameOpt) > 0 { - filename = filenameOpt[0] - } - return intent.UploadMedia(ctx, roomID, data, filename, mimeType) +type AnimatedStickerConfig struct { + Target string `yaml:"target"` + ConvertFromWebm bool `yaml:"convert_from_webm"` + Args struct { + Width int `yaml:"width"` + Height int `yaml:"height"` + FPS int `yaml:"fps"` + } `yaml:"args"` +} + +func (c AnimatedStickerConfig) TGSConvert() bool { + return c.Target == "gif" || c.Target == "png" +} + +func (c AnimatedStickerConfig) WebmConvert() bool { + return c.ConvertFromWebm && c.Target != "webm" +} + +type Transferer struct { + RoomID id.RoomID + Filename string + IsSticker bool + Config AnimatedStickerConfig +} + +func NewTransferer(cfg AnimatedStickerConfig) *Transferer { + return &Transferer{Config: cfg} +} + +func (t *Transferer) WithRoomID(roomID id.RoomID) *Transferer { + t.RoomID = roomID + return t +} + +func (t *Transferer) WithFilename(filename string) *Transferer { + t.Filename = filename + return t +} + +func (t *Transferer) WithIsSticker(isSticker bool) *Transferer { + t.IsSticker = isSticker + return t +} + +func (t *Transferer) Transfer(ctx context.Context, store *store.Container, client downloader.Client, intent bridgev2.MatrixAPI, loc tg.InputFileLocationClass) (mxc id.ContentURIString, encryptedFileInfo *event.EncryptedFileInfo, size int, mimeType string, err error) { + locationID := getLocationID(loc) + if file, err := store.TelegramFile.GetByLocationID(ctx, locationID); err != nil { + return "", nil, 0, "", fmt.Errorf("failed to search for Telegram file by location ID: %w", err) + } else if file != nil { + return file.MXC, nil, file.Size, file.MIMEType, nil + } + + var data []byte + data, mimeType, err = DownloadFileLocation(ctx, client, loc) + if err != nil { + return "", nil, 0, "", fmt.Errorf("downloading file failed: %w", err) + } + + if t.IsSticker { + if lottie.Supported() && t.Config.TGSConvert() && mimeType == "application/x-gzip" { + data, err = lottie.ConvertBytes(ctx, data, t.Config.Target, t.Config.Args.Width, t.Config.Args.Height, fmt.Sprintf("%d", t.Config.Args.FPS)) + if err != nil { + return "", nil, 0, "", err + } + mimeType = fmt.Sprintf("image/%s", t.Config.Target) + // TODO support ffmpeg conversion + // } else if ffmpeg.Supported() && t.Config.WebmConvert() && mimeType == "video/webm" { + } + } + + mxcURI, encryptedFileInfo, err := intent.UploadMedia(ctx, t.RoomID, data, t.Filename, mimeType) + if err != nil { + return "", nil, 0, "", err + } + if len(mxcURI) > 0 { + file := store.TelegramFile.New() + file.LocationID = locationID + file.MXC = mxcURI + file.Size = len(data) + file.MIMEType = mimeType + // TODO width, height, thumbnail? + if err = file.Insert(ctx); err != nil { + return "", nil, 0, "", fmt.Errorf("failed to insert Telegram file into database: %w", err) + } + } + return mxcURI, encryptedFileInfo, len(data), mimeType, nil } diff --git a/pkg/connector/msgconv/converter.go b/pkg/connector/msgconv/converter.go index b9b1313c..940a6833 100644 --- a/pkg/connector/msgconv/converter.go +++ b/pkg/connector/msgconv/converter.go @@ -3,15 +3,26 @@ package msgconv import ( "github.com/gotd/td/telegram" "maunium.net/go/mautrix/bridgev2" + + "go.mau.fi/mautrix-telegram/pkg/connector/media" + "go.mau.fi/mautrix-telegram/pkg/connector/store" ) type MessageConverter struct { - client *telegram.Client - connector bridgev2.MatrixConnector + client *telegram.Client + connector bridgev2.MatrixConnector + store *store.Container + animatedStickerConfig media.AnimatedStickerConfig useDirectMedia bool } -func NewMessageConverter(client *telegram.Client, connector bridgev2.MatrixConnector, useDirectMedia bool) *MessageConverter { - return &MessageConverter{client: client, connector: connector, useDirectMedia: useDirectMedia} +func NewMessageConverter(client *telegram.Client, connector bridgev2.MatrixConnector, store *store.Container, animatedStickerConfig media.AnimatedStickerConfig, useDirectMedia bool) *MessageConverter { + return &MessageConverter{ + client: client, + connector: connector, + store: store, + animatedStickerConfig: animatedStickerConfig, + useDirectMedia: useDirectMedia, + } } diff --git a/pkg/connector/msgconv/tomatrix.go b/pkg/connector/msgconv/tomatrix.go index 000709de..b3295ea4 100644 --- a/pkg/connector/msgconv/tomatrix.go +++ b/pkg/connector/msgconv/tomatrix.go @@ -208,12 +208,14 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por } if info.ThumbnailInfo.ThumbnailURL == "" { - info.ThumbnailInfo.ThumbnailURL, info.ThumbnailInfo.ThumbnailFile, err = media.TransferToMatrix(ctx, portal.MXID, mc.client.API(), intent, &tg.InputDocumentFileLocation{ - ID: document.GetID(), - AccessHash: document.GetAccessHash(), - FileReference: document.GetFileReference(), - ThumbSize: largestThumbnail.GetType(), - }) + info.ThumbnailInfo.ThumbnailURL, info.ThumbnailInfo.ThumbnailFile, info.ThumbnailInfo.Size, info.ThumbnailInfo.MimeType, err = media.NewTransferer(mc.animatedStickerConfig). + WithRoomID(portal.MXID). + Transfer(ctx, mc.store, mc.client.API(), intent, &tg.InputDocumentFileLocation{ + ID: document.GetID(), + AccessHash: document.GetAccessHash(), + FileReference: document.GetFileReference(), + ThumbSize: largestThumbnail.GetType(), + }) if err != nil { return nil, nil, err } @@ -256,24 +258,23 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por if mxcURI == "" { var data []byte - var mimeType string - var err error switch msgMedia := msgMedia.(type) { case *tg.MessageMediaPhoto: + // TODO convert to Transfer + data, _, _, info.MimeType, err = media.DownloadPhotoMedia(ctx, mc.client.API(), msgMedia) if _, ok := msgMedia.GetTTLSeconds(); ok { - filename = "disappearing_image" + exmime.ExtensionFromMimetype(mimeType) + filename = "disappearing_image" + exmime.ExtensionFromMimetype(info.MimeType) } else { - filename = "image" + exmime.ExtensionFromMimetype(mimeType) + filename = "image" + exmime.ExtensionFromMimetype(info.MimeType) } - - data, _, _, mimeType, err = media.DownloadPhotoMedia(ctx, mc.client.API(), msgMedia) case *tg.MessageMediaDocument: document, ok := msgMedia.Document.(*tg.Document) if !ok { return nil, nil, fmt.Errorf("unrecognized document type %T", msgMedia.Document) } - mimeType = document.GetMimeType() + info.MimeType = document.GetMimeType() + // TODO convert to Transfer data, err = media.DownloadDocument(ctx, mc.client.API(), document) default: return nil, nil, fmt.Errorf("unhandled media type %T", msgMedia) @@ -282,7 +283,7 @@ func (mc *MessageConverter) convertMediaRequiringUpload(ctx context.Context, por return nil, nil, err } - mxcURI, encryptedFileInfo, err = intent.UploadMedia(ctx, portal.MXID, data, filename, mimeType) + mxcURI, encryptedFileInfo, err = intent.UploadMedia(ctx, portal.MXID, data, filename, info.MimeType) if err != nil { return nil, nil, err } diff --git a/pkg/connector/store/telegramfile.go b/pkg/connector/store/telegramfile.go index f509fb89..d212280e 100644 --- a/pkg/connector/store/telegramfile.go +++ b/pkg/connector/store/telegramfile.go @@ -2,23 +2,14 @@ package store import ( "context" - "database/sql" - "encoding/json" - "time" "go.mau.fi/util/dbutil" + "maunium.net/go/mautrix/id" ) const ( - insertTelegramFileQuery = ` - INSERT INTO telegram_file ( - id, mxc, mime_type, was_converted, timestamp, size, width, height, thumbnail, decryption_info) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) - ` - getTelegramFileSelect = ` - SELECT id, mxc, mime_type, was_converted, timestamp, size, width, height, thumbnail, decryption_info - FROM telegram_file - ` + insertTelegramFileQuery = "INSERT INTO telegram_file (id, mxc, mime_type, size) VALUES ($1, $2, $3, $4)" + getTelegramFileSelect = "SELECT id, mxc, mime_type, size FROM telegram_file " getTelegramFileByLocationIDQuery = getTelegramFileSelect + "WHERE id=$1" getTelegramFileByMXCQuery = getTelegramFileSelect + "WHERE mxc=$1" ) @@ -32,16 +23,10 @@ type TelegramFileLocationID string type TelegramFile struct { qh *dbutil.QueryHelper[*TelegramFile] - LocationID TelegramFileLocationID - MXC string - MimeType string - WasConverted bool - Timestamp time.Time - Size int64 - Width int - Height int - ThumbnailID string - DecryptionInfo json.RawMessage + LocationID TelegramFileLocationID + MXC id.ContentURIString + MIMEType string + Size int } var _ dbutil.DataStruct[*TelegramFile] = (*TelegramFile)(nil) @@ -50,7 +35,7 @@ func newTelegramFile(qh *dbutil.QueryHelper[*TelegramFile]) *TelegramFile { return &TelegramFile{qh: qh} } -func (fq *TelegramFileQuery) GetByLocationID(ctx context.Context, locationID string) (*TelegramFile, error) { +func (fq *TelegramFileQuery) GetByLocationID(ctx context.Context, locationID TelegramFileLocationID) (*TelegramFile, error) { return fq.QueryOne(ctx, getTelegramFileByLocationIDQuery, locationID) } @@ -59,18 +44,7 @@ func (fq *TelegramFileQuery) GetByMXC(ctx context.Context, mxc string) (*Telegra } func (f *TelegramFile) sqlVariables() []any { - return []any{ - f.LocationID, - f.MXC, - f.MimeType, - f.WasConverted, - f.Timestamp.UnixMilli(), - f.Size, - f.Width, - f.Height, - f.ThumbnailID, - f.DecryptionInfo, - } + return []any{f.LocationID, f.MXC, f.MIMEType, f.Size} } func (f *TelegramFile) Insert(ctx context.Context) error { @@ -78,21 +52,5 @@ func (f *TelegramFile) Insert(ctx context.Context) error { } func (f *TelegramFile) Scan(row dbutil.Scannable) (*TelegramFile, error) { - var thumbnailID sql.NullString - var timestamp int64 - err := row.Scan( - &f.LocationID, - &f.MXC, - &f.MimeType, - &f.WasConverted, - ×tamp, - &f.Size, - &f.Width, - &f.Height, - &thumbnailID, - &f.DecryptionInfo, - ) - f.Timestamp = time.UnixMilli(timestamp) - f.ThumbnailID = thumbnailID.String - return f, err + return f, row.Scan(&f.LocationID, &f.MXC, &f.MIMEType, &f.Size) } diff --git a/pkg/connector/store/upgrades/00-latest.sql b/pkg/connector/store/upgrades/00-latest.sql index e7886eb4..40da818e 100644 --- a/pkg/connector/store/upgrades/00-latest.sql +++ b/pkg/connector/store/upgrades/00-latest.sql @@ -32,16 +32,8 @@ CREATE TABLE telegram_channel_access_hashes ( ); CREATE TABLE telegram_file ( - id TEXT PRIMARY KEY, - mxc TEXT NOT NULL, - mime_type TEXT, - was_converted BOOLEAN NOT NULL DEFAULT false, - timestamp BIGINT NOT NULL DEFAULT 0, - size BIGINT, - width INTEGER, - height INTEGER, - thumbnail TEXT, - decryption_info jsonb, - FOREIGN KEY (thumbnail) REFERENCES telegram_file(id) - ON UPDATE CASCADE ON DELETE SET NULL + id TEXT PRIMARY KEY, + mxc TEXT NOT NULL, + mime_type TEXT, + size BIGINT ); diff --git a/pkg/connector/telegram.go b/pkg/connector/telegram.go index 97a36687..225ce9d5 100644 --- a/pkg/connector/telegram.go +++ b/pkg/connector/telegram.go @@ -15,6 +15,7 @@ import ( "go.mau.fi/mautrix-telegram/pkg/connector/emojis" "go.mau.fi/mautrix-telegram/pkg/connector/ids" + "go.mau.fi/mautrix-telegram/pkg/connector/media" "go.mau.fi/mautrix-telegram/pkg/connector/util" ) @@ -225,7 +226,6 @@ func (t *TelegramClient) handleTelegramReactions(ctx context.Context, msg *tg.Me if _, set := msg.GetReactions(); !set { return nil } - fmt.Printf("handle reactions %+v\n", msg.Reactions) var totalCount int for _, r := range msg.Reactions.Results { totalCount += r.Count @@ -253,6 +253,8 @@ func (t *TelegramClient) handleTelegramReactions(ctx context.Context, msg *tg.Me if len(reactionsList) < totalCount { if user, ok := msg.PeerID.(*tg.PeerUser); ok { reactionsList = splitDMReactionCounts(msg.Reactions.Results, user.UserID, t.loginID) + + // TODO // } else if t.isBot { // // Can't fetch exact reaction senders as a bot // return @@ -325,25 +327,34 @@ func (t *TelegramClient) getReactionLimit(ctx context.Context, sender networkid. return 1, nil } -// TODO move this to emojis package func (t *TelegramClient) transferEmojisToMatrix(ctx context.Context, customEmojiIDs []int64) (result map[networkid.EmojiID]string, err error) { result, customEmojiIDs = emojis.ConvertKnownEmojis(customEmojiIDs) - for _, customEmojiID := range customEmojiIDs { - fmt.Printf("customEmojiID %d\n", customEmojiID) - locationID := fmt.Sprintf("%d", customEmojiID) - if file, err := t.main.Store.TelegramFile.GetByLocationID(ctx, locationID); err != nil { - return nil, fmt.Errorf("failed to search for Telegram file by location ID") - } else if file == nil { - // TODO download shit - } else { - result[ids.MakeEmojiIDFromDocumentID(customEmojiID)] = file.MXC + + if len(customEmojiIDs) > 0 { + customEmojiDocuments, err := t.client.API().MessagesGetCustomEmojiDocuments(ctx, customEmojiIDs) + if err != nil { + return nil, err + } + + for _, customEmojiDocument := range customEmojiDocuments { + document := customEmojiDocument.(*tg.Document) + mxcURI, _, _, _, err := media.NewTransferer(t.main.Config.AnimatedSticker). + WithIsSticker(true). + Transfer(ctx, t.main.Store, t.client.API(), t.main.Bridge.Bot, &tg.InputDocumentFileLocation{ + ID: document.GetID(), + AccessHash: document.GetAccessHash(), + FileReference: document.GetFileReference(), + }) + if err != nil { + return nil, err + } + result[ids.MakeEmojiIDFromDocumentID(document.ID)] = string(mxcURI) } } return } func (t *TelegramClient) handleTelegramParsedReactionsLocked(ctx context.Context, msg *database.Message, reactions map[networkid.UserID][]tg.MessagePeerReaction, customEmojiIDs []int64, isFull bool, onlyUserID *networkid.UserID, timestamp *time.Time) error { - // TODO deal with the custom emoji IDs customEmojis, err := t.transferEmojisToMatrix(ctx, customEmojiIDs) if err != nil { return err @@ -359,9 +370,8 @@ func (t *TelegramClient) handleTelegramParsedReactionsLocked(ctx context.Context if onlyUserID != nil && existing.SenderID != *onlyUserID { continue } - newReactions := reactions[existing.SenderID] var matched bool - reactions[existing.SenderID], matched, err = reactionsFilter(newReactions, existing) + reactions[existing.SenderID], matched, err = reactionsFilter(reactions[existing.SenderID], existing) if err != nil { return err } else if !matched { @@ -369,7 +379,7 @@ func (t *TelegramClient) handleTelegramParsedReactionsLocked(ctx context.Context removed = append(removed, existing) } else if reactionLimit, err := t.getReactionLimit(ctx, existing.SenderID); err != nil { return err - } else if len(newReactions) == reactionLimit { + } else if len(reactions[existing.SenderID]) >= reactionLimit { removed = append(removed, existing) } } @@ -447,7 +457,6 @@ func (t *TelegramClient) handleTelegramParsedReactionsLocked(ctx context.Context t.main.Bridge.QueueRemoteEvent(t.userLogin, evt) } - fmt.Printf("%v %v %v\n", isFull, reactions, customEmojiIDs) return nil }