diff --git a/bridge/telegram/handlers.go b/bridge/telegram/handlers.go index b857b56d5..9ab0d2be8 100644 --- a/bridge/telegram/handlers.go +++ b/bridge/telegram/handlers.go @@ -129,139 +129,141 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag } // handleUsername handles the correct setting of the username -func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { - if message.From != nil { - rmsg.UserID = strconv.FormatInt(message.From.ID, 10) - if b.GetBool("UseFirstName") { - rmsg.Username = message.From.FirstName - } - if b.GetBool("UseFullName") { - if message.From.FirstName != "" && message.From.LastName != "" { - rmsg.Username = message.From.FirstName + " " + message.From.LastName - } - } - if rmsg.Username == "" { - rmsg.Username = message.From.UserName - if rmsg.Username == "" { - rmsg.Username = message.From.FirstName - } - } - // only download avatars if we have a place to upload them (configured mediaserver) - if b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "" { - b.handleDownloadAvatar(message.From.ID, rmsg.Channel) - } +// +// This method may block due to downloading the remote avatar. +func (b *Btelegram) handleUsernameBlocking(rmsg *config.Message, message *tgbotapi.Message) { + var ( + firstName, lastName, userName string + id int64 + ) + + switch { + case message.From != nil: + firstName = message.From.FirstName + lastName = message.From.LastName + userName = message.From.UserName + id = message.From.ID + case message.SenderChat != nil: + // TODO: here previous code was checking for rmsg.Username == "Channel_Bot" + // and performing what i believe was unnecessary steps. If problems arise, + // look at that first. + firstName = message.SenderChat.FirstName + lastName = message.SenderChat.LastName + userName = message.SenderChat.UserName + id = message.SenderChat.ID } - if message.SenderChat != nil { //nolint:nestif - rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10) - if b.GetBool("UseFirstName") { - rmsg.Username = message.SenderChat.FirstName - } - if b.GetBool("UseFullName") { - if message.SenderChat.FirstName != "" && message.SenderChat.LastName != "" { - rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName - } - } + if b.GetBool("UseFirstName") { + rmsg.Username = firstName + } - if rmsg.Username == "" || rmsg.Username == "Channel_Bot" { - rmsg.Username = message.SenderChat.UserName + if b.GetBool("UseFullName") && lastName != "" { + rmsg.Username = rmsg.Username + " " + lastName + } - if rmsg.Username == "" || rmsg.Username == "Channel_Bot" { - rmsg.Username = message.SenderChat.FirstName - } - } - // only download avatars if we have a place to upload them (configured mediaserver) - if b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "" { - b.handleDownloadAvatar(message.SenderChat.ID, rmsg.Channel) + if rmsg.Username == "" { + if userName != "" { + rmsg.Username = userName + } else { + // if we really didn't find a username, set it to unknown + rmsg.Username = unknownUser } } + rmsg.UserID = strconv.FormatInt(id, 10) + b.handleDownloadAvatarBlocking(id, rmsg.Channel) + // Fallback on author signature (used in "channel" type of chat) if rmsg.Username == "" && message.AuthorSignature != "" { rmsg.Username = message.AuthorSignature } - - // if we really didn't find a username, set it to unknown - if rmsg.Username == "" { - rmsg.Username = unknownUser - } } +// All messages are processed in the background, because attachments +// need to be processed in the background, but avatars too. +// +// We don't want to block the main thread! func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) { for update := range updates { b.Log.Debugf("== Receiving event: %#v", update.Message) - if update.Message == nil && update.ChannelPost == nil && - update.EditedMessage == nil && update.EditedChannelPost == nil { - b.Log.Info("Received event without messages, skipping.") - continue - } + go b.handleRecvBackground(update) + } +} - if b.GetInt("debuglevel") == 1 { - spew.Dump(update.Message) - } +func (b *Btelegram) handleRecvBackground(update tgbotapi.Update) { + if update.Message == nil && update.ChannelPost == nil && + update.EditedMessage == nil && update.EditedChannelPost == nil { + b.Log.Info("Received event without messages, skipping.") + return + } - b.handleGroupUpdate(update) + if b.GetInt("debuglevel") == 1 { + spew.Dump(update.Message) + } - var message *tgbotapi.Message + b.handleGroupUpdate(update) - rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})} + var message *tgbotapi.Message - // handle channels - message = b.handleChannels(&rmsg, message, update) + rmsg := config.Message{Account: b.Account, Extra: make(map[string][]any)} - // handle groups - message = b.handleGroups(&rmsg, message, update) + // handle channels + message = b.handleChannels(&rmsg, message, update) - if message == nil { - b.Log.Error("message is nil, this shouldn't happen.") - continue - } + // handle groups + message = b.handleGroups(&rmsg, message, update) - // set the ID's from the channel or group message - rmsg.ID = strconv.Itoa(message.MessageID) - rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10) - if message.IsTopicMessage { - rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID) - } + if message == nil { + b.Log.Error("message is nil, this shouldn't happen.") + return + } - // preserve threading from telegram reply - if message.ReplyToMessage != nil && - // Used to check if the message was a reply to the root topic - (!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { - rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID) - } + // set the ID's from the channel or group message + rmsg.ID = strconv.Itoa(message.MessageID) - // handle entities (adding URLs) - b.handleEntities(&rmsg, message) + rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10) + if message.IsTopicMessage { + rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID) + } - // handle username - b.handleUsername(&rmsg, message) + // preserve threading from telegram reply + if message.ReplyToMessage != nil && + // Used to check if the message was a reply to the root topic + (!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { + rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID) + } - // handle any downloads - err := b.handleDownload(&rmsg, message) - if err != nil { - b.Log.Errorf("download failed: %s", err) - } + // handle entities (adding URLs) + b.handleEntities(&rmsg, message) - // handle forwarded messages - b.handleForwarded(&rmsg, message) + // handle username + b.handleUsernameBlocking(&rmsg, message) - // quote the previous message - b.handleQuoting(&rmsg, message) + // File downloads are handled in the background + err := b.handleDownloadBlocking(&rmsg, message) + if err != nil { + b.Log.Errorf("download failed: %s", err) + } - if rmsg.Text != "" || len(rmsg.Extra) > 0 { - // Comment the next line out due to avoid removing empty lines in Telegram - // rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) - // channels don't have (always?) user information. see #410 - if message.From != nil { - rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General) - } + // handle forwarded messages + b.handleForwarded(&rmsg, message) + + // quote the previous message + b.handleQuoting(&rmsg, message) - b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) - b.Log.Debugf("<= Message is %#v", rmsg) - b.Remote <- rmsg + if rmsg.Text != "" || len(rmsg.Extra) > 0 { + // Comment the next line out due to avoid removing empty lines in Telegram + // rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) + // channels don't have (always?) user information. see #410 + if message.From != nil { + rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General) } + + b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) + b.Log.Debugf("<= Message is %#v", rmsg) + + b.Remote <- rmsg } } @@ -309,10 +311,10 @@ func (b *Btelegram) handleUserLeave(update tgbotapi.Update) { b.Remote <- rmsg } -// handleDownloadAvatar downloads the avatar of userid from channel +// handleDownloadAvatarBlocking downloads the avatar of userid from channel // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. // logs an error message if it fails -func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) { +func (b *Btelegram) handleDownloadAvatarBlocking(userid int64, channel string) { rmsg := config.Message{ Username: "system", Text: "avatar", @@ -383,31 +385,28 @@ func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) { } } -// handleDownloadFile handles file download -func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error { - size := int64(0) +// handleDownloadBlockin handles file download +func (b *Btelegram) handleDownloadBlocking(rmsg *config.Message, message *tgbotapi.Message) error { var url, name, text string switch { case message.Sticker != nil: text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true) - size = int64(message.Sticker.FileSize) case message.Voice != nil: text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true) - size = message.Voice.FileSize case message.Video != nil: text, name, url = b.getDownloadInfo(message.Video.FileID, "", true) - size = message.Video.FileSize case message.Audio != nil: text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true) - size = message.Audio.FileSize + // rename .oga to .ogg https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512 + if strings.HasSuffix(name, ".oga") { + name = strings.Replace(name, ".oga", ".ogg", 1) + } case message.Document != nil: _, _, url = b.getDownloadInfo(message.Document.FileID, "", false) - size = message.Document.FileSize name = message.Document.FileName text = " " + message.Document.FileName + " : " + url case message.Photo != nil: photos := message.Photo - size = int64(photos[len(photos)-1].FileSize) text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) } @@ -421,31 +420,37 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa rmsg.Text += text return nil } - // if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra - err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General) - if err != nil { - return err - } - data, err := helper.DownloadFile(url) + + err := b.AddAttachmentFromURL(rmsg, name, "", message.Caption, url) if err != nil { return err } - if strings.HasSuffix(name, ".tgs.webp") { - b.maybeConvertTgs(&name, data) - } else if strings.HasSuffix(name, ".webp") { - b.maybeConvertWebp(&name, data) - } - - // rename .oga to .ogg https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512 - if strings.HasSuffix(name, ".oga") && message.Audio != nil { - name = strings.Replace(name, ".oga", ".ogg", 1) - } + // Perform file format conversions for interop + b.handleDownloadPostProcessBlocking(rmsg) - helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General) return nil } +func (b *Btelegram) handleDownloadPostProcessBlocking(rmsg *config.Message) { + // TODO: maybe this could be moved to a new helper taking a function/closure + // to perform post-download processing. + for _, f := range rmsg.Extra["file"] { + fi, ok := f.(config.FileInfo) + if !ok { + continue + } + + // Now that we have the file bytes, we may have some conversions to do + // TODO: I don't think f.SHA is computed already but make sure of it + if strings.HasSuffix(fi.Name, ".tgs.webp") { + b.maybeConvertTgs(&fi.Name, fi.Data) + } else if strings.HasSuffix(fi.Name, ".webp") { + b.maybeConvertWebp(&fi.Name, fi.Data) + } + } +} + func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) { url := b.getFileDirectURL(id) name := "" diff --git a/bridge/telegram/telegram.go b/bridge/telegram/telegram.go index 8c15486f9..dbfc349fc 100644 --- a/bridge/telegram/telegram.go +++ b/bridge/telegram/telegram.go @@ -118,6 +118,31 @@ func (b *Btelegram) getIds(channel string) (int64, int, error) { return chatid, topicid, nil } +// Send TODO: We process the message in the background, +// but now we can't return the produced message IDs. +// +// Also, if there are several attachments, `SendMediaGroup` will +// return several messages, but we currently only get 1 message ID +// to store on matterbridge side. Unless the method returns a +// sort of parent message which will aggregate all replies +// (which may be the case, but if so is undocumented), we want +// to map all telegram replies to any of those messages +// back to the original message. +// +// We need to perform more logic somehow with new helpers/channels +// Currently, the logic for saving message IDs is in gateway/handlers.go +// in the `handleMessage` function, returning IDs to +// `gateway/routeur.go:handleReceive` which in turn will +// save all gateway produced message IDs to associate with the +// original message ID being broadcast. It seems like there is no +// gateway-specific association/typing in there so collisions +// between networks is possible???? +// +// So we probably want a new helper like `b.RegisterMessageID(string, []string)` +// which individual bridges can call once they know an association should be +// performed, and which should just silently ignore the case when `rmsg.ID` +// (first argument) is an empty string. (ideally, we'd like to separate IDs +// of different networks, but well let's not get ahead of ourselves). func (b *Btelegram) Send(msg config.Message) (string, error) { b.Log.Debugf("=> Receiving %#v", msg) @@ -135,9 +160,19 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { msg.Text = makeHTML(html.EscapeString(msg.Text)) } + // Perform blocking operations in the background + go b.SendBlocking(&msg, chatid, topicid) + + // TODO: see big TODO above about message ID correctness + return "", nil +} + +func (b *Btelegram) SendBlocking(msg *config.Message, chatid int64, topicid int) { + // TODO: Maybe still produce an error that can be logged by the Send method? + // Delete message if msg.Event == config.EventMsgDelete { - return b.handleDelete(&msg, chatid) + _, _ = b.handleDelete(msg, chatid) } // Handle prefix hint for unthreaded messages. @@ -153,20 +188,20 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { // Upload a file if it exists if msg.Extra != nil { - for _, rmsg := range helper.HandleExtra(&msg, b.General) { + for _, rmsg := range helper.HandleExtra(msg, b.General) { if _, msgErr := b.sendMessage(chatid, topicid, rmsg.Username, rmsg.Text, parentID); msgErr != nil { b.Log.Errorf("sendMessage failed: %s", msgErr) } } // check if we have files to upload (from slack, telegram or mattermost) if len(msg.Extra["file"]) > 0 { - return b.handleUploadFile(&msg, chatid, topicid, parentID) + _, _ = b.handleUploadFile(msg, chatid, topicid, parentID) } } // edit the message if we have a msg ID if msg.ID != "" { - return b.handleEdit(&msg, chatid) + _, _ = b.handleEdit(msg, chatid) } // Post normal message @@ -174,10 +209,8 @@ func (b *Btelegram) Send(msg config.Message) (string, error) { // Ignore empty text field needs for prevent double messages from whatsapp to telegram // when sending media with text caption if msg.Text != "" { - return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID) + _, _ = b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID) } - - return "", nil } func (b *Btelegram) getFileDirectURL(id string) string {