From 4532748c8458971151dfb6b535b11b2a3e17a372 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 10 Jan 2024 14:30:00 -0500 Subject: [PATCH] Support chosen quotes in replies and replies from other chats --- telegram/commands.go | 2 +- telegram/utils.go | 213 +++++++++++++++++++++++++++-------------- telegram/utils_test.go | 95 +++++++++--------- 3 files changed, 195 insertions(+), 115 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 48c3615..c4b5988 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -196,7 +196,7 @@ func (c *Client) unsubscribe(chatID int64) error { func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { for i := len(messages) - 1; i >= 0; i-- { message := messages[i] - reply, _ := c.getMessageReply(message) + reply, _ := c.getMessageReply(message, false, true) gateway.SendMessage( c.jid, diff --git a/telegram/utils.go b/telegram/utils.go index 9370839..b17d692 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -37,6 +37,14 @@ type VCardInfo struct { Info string } +type messageStub struct { + MessageId int64 + ChatId int64 + Sender string + Date int32 + Text string +} + var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) @@ -342,33 +350,74 @@ func (c *Client) formatSender(message *client.Message) string { return c.formatContact(c.getSenderId(message)) } -func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyMsg *client.Message) { +func (c *Client) messageToStub(message *client.Message, preview bool, text string) *messageStub { + if text == "" { + text = c.messageContentToText(message.Content, message.ChatId, preview) + } + return &messageStub{ + MessageId: message.Id, + ChatId: message.ChatId, + Sender: c.formatSender(message), + Date: message.Date, + Text: text, + } +} + +func (c *Client) getMessageReply(message *client.Message, preview bool, noContent bool) (gatewayReply *gateway.Reply, tgReply *messageStub) { if message.ReplyTo != nil && message.ReplyTo.MessageReplyToType() == client.TypeMessageReplyToMessage { replyTo, _ := message.ReplyTo.(*client.MessageReplyToMessage) - // TODO: support replies from other chats - if message.ChatId != replyTo.ChatId { - log.Warn("Reply from other/unknown chat") - log.Debugf("replyTo: %#v", replyTo) - return + var text string + if replyTo.Quote != nil && !noContent { + text = formatter.Format( + replyTo.Quote.Text, + replyTo.Quote.Entities, + c.getFormatter(), + ) + // make the whole quote fit one line + text = strings.ReplaceAll(text, "\n", " ") } + if message.ChatId == replyTo.ChatId { + // obtain message from this chat + replyMsg, err := c.client.GetMessage(&client.GetMessageRequest{ + ChatId: message.ChatId, + MessageId: replyTo.MessageId, + }) + if err != nil { + log.Errorf("", err.Error()) + return + } - var err error - replyMsg, err = c.client.GetMessage(&client.GetMessageRequest{ - ChatId: message.ChatId, - MessageId: replyTo.MessageId, - }) - if err != nil { - log.Errorf("", err.Error()) - return - } + if !noContent { + tgReply = c.messageToStub(replyMsg, preview, text) + } - replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, replyTo.MessageId) - if err != nil { - replyId = strconv.FormatInt(replyTo.MessageId, 10) - } - reply = &gateway.Reply{ - Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), - Id: replyId, + replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, replyTo.MessageId) + if err != nil { + replyId = strconv.FormatInt(replyTo.MessageId, 10) + } + + gatewayReply = &gateway.Reply{ + Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), + Id: replyId, + } + } else if !noContent { + // it's safe to assume there's no need to pass ChatId here + // as it's needed only for pin messages which are not allowed in replies + if text == "" && replyTo.Content != nil { + text = c.messageContentToText(replyTo.Content, 0, preview) + } + + if text == "" { + log.Error("Empty reply from other/unknown chat") + log.Debugf("replyTo: %#v", replyTo) + return + } + + tgReply = &messageStub{ + Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.formatContact(replyTo.ChatId), + Date: replyTo.OriginSendDate, + Text: text, + } } } @@ -391,9 +440,16 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess return "" } + return c.formatMessageContent(preview, c.messageToStub(message, preview, "")) +} + +func (c *Client) formatMessageContent(preview bool, message *messageStub) string { var str strings.Builder // add messageid and sender - str.WriteString(fmt.Sprintf("%v | %s | ", message.Id, c.formatSender(message))) + if message.MessageId != 0 { + str.WriteString(fmt.Sprintf("%v | ", message.MessageId)) + } + str.WriteString(fmt.Sprintf("%s | ", message.Sender)) // add date if !preview { str.WriteString( @@ -404,10 +460,7 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess } // text message - var text string - if message.Content != nil { - text = c.messageToText(message, preview) - } + text := message.Text if text != "" { if !preview { str.WriteString(text) @@ -424,30 +477,33 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess return str.String() } -func (c *Client) formatForward(fwd *client.MessageForwardInfo) string { - switch fwd.Origin.MessageOriginType() { +func (c *Client) formatOrigin(origin client.MessageOrigin) string { + if origin == nil { + return "" + } + switch origin.MessageOriginType() { case client.TypeMessageOriginUser: - originUser := fwd.Origin.(*client.MessageOriginUser) + originUser := origin.(*client.MessageOriginUser) return c.formatContact(originUser.SenderUserId) case client.TypeMessageOriginChat: - originChat := fwd.Origin.(*client.MessageOriginChat) + originChat := origin.(*client.MessageOriginChat) var signature string if originChat.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) } return c.formatContact(originChat.SenderChatId) + signature case client.TypeMessageOriginHiddenUser: - originUser := fwd.Origin.(*client.MessageOriginHiddenUser) + originUser := origin.(*client.MessageOriginHiddenUser) return originUser.SenderName case client.TypeMessageOriginChannel: - channel := fwd.Origin.(*client.MessageOriginChannel) + channel := origin.(*client.MessageOriginChannel) var signature string if channel.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) } return c.formatContact(channel.ChatId) + signature } - return "Unknown forward type" + return "Unknown origin type" } func (c *Client) formatFile(file *client.File, compact bool) (string, string) { @@ -593,20 +649,24 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return "" } + return c.messageContentToText(message.Content, message.ChatId, preview) +} + +func (c *Client) messageContentToText(content client.MessageContent, chatId int64, preview bool) string { markupMode := c.getFormatter() - switch message.Content.MessageContentType() { + switch content.MessageContentType() { case client.TypeMessageSticker: - sticker, _ := message.Content.(*client.MessageSticker) + sticker, _ := content.(*client.MessageSticker) return sticker.Sticker.Emoji case client.TypeMessageAnimatedEmoji: - animatedEmoji, _ := message.Content.(*client.MessageAnimatedEmoji) + animatedEmoji, _ := content.(*client.MessageAnimatedEmoji) return animatedEmoji.Emoji case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate: return "has created chat" case client.TypeMessageChatJoinByLink: return "joined chat via invite link" case client.TypeMessageChatAddMembers: - addMembers, _ := message.Content.(*client.MessageChatAddMembers) + addMembers, _ := content.(*client.MessageChatAddMembers) text := "invited " if len(addMembers.MemberUserIds) > 0 { @@ -615,19 +675,19 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return text case client.TypeMessageChatDeleteMember: - deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) + deleteMember, _ := content.(*client.MessageChatDeleteMember) return "kicked " + c.formatContact(deleteMember.UserId) case client.TypeMessagePinMessage: - pinMessage, _ := message.Content.(*client.MessagePinMessage) - return "pinned message: " + c.formatMessage(message.ChatId, pinMessage.MessageId, preview, nil) + pinMessage, _ := content.(*client.MessagePinMessage) + return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, nil) case client.TypeMessageChatChangeTitle: - changeTitle, _ := message.Content.(*client.MessageChatChangeTitle) + changeTitle, _ := content.(*client.MessageChatChangeTitle) return "chat title set to: " + changeTitle.Title case client.TypeMessageLocation: - location, _ := message.Content.(*client.MessageLocation) + location, _ := content.(*client.MessageLocation) return c.formatLocation(location.Location) case client.TypeMessageVenue: - venue, _ := message.Content.(*client.MessageVenue) + venue, _ := content.(*client.MessageVenue) if preview { return venue.Venue.Title } else { @@ -639,7 +699,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessagePhoto: - photo, _ := message.Content.(*client.MessagePhoto) + photo, _ := content.(*client.MessagePhoto) if preview { return photo.Caption.Text } else { @@ -650,7 +710,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessageAudio: - audio, _ := message.Content.(*client.MessageAudio) + audio, _ := content.(*client.MessageAudio) if preview { return audio.Caption.Text } else { @@ -661,7 +721,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessageVideo: - video, _ := message.Content.(*client.MessageVideo) + video, _ := content.(*client.MessageVideo) if preview { return video.Caption.Text } else { @@ -672,7 +732,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessageDocument: - document, _ := message.Content.(*client.MessageDocument) + document, _ := content.(*client.MessageDocument) if preview { return document.Caption.Text } else { @@ -683,7 +743,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessageText: - text, _ := message.Content.(*client.MessageText) + text, _ := content.(*client.MessageText) if preview { return text.Text.Text } else { @@ -694,7 +754,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessageVoiceNote: - voice, _ := message.Content.(*client.MessageVoiceNote) + voice, _ := content.(*client.MessageVoiceNote) if preview { return voice.Caption.Text } else { @@ -707,7 +767,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { case client.TypeMessageVideoNote: return "" case client.TypeMessageAnimation: - animation, _ := message.Content.(*client.MessageAnimation) + animation, _ := content.(*client.MessageAnimation) if preview { return animation.Caption.Text } else { @@ -718,7 +778,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessageContact: - contact, _ := message.Content.(*client.MessageContact) + contact, _ := content.(*client.MessageContact) if preview { return contact.Contact.FirstName + " " + contact.Contact.LastName } else { @@ -736,10 +796,10 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { ) } case client.TypeMessageDice: - dice, _ := message.Content.(*client.MessageDice) + dice, _ := content.(*client.MessageDice) return fmt.Sprintf("%s 1d6: [%v]", dice.Emoji, dice.Value) case client.TypeMessagePoll: - poll, _ := message.Content.(*client.MessagePoll) + poll, _ := content.(*client.MessagePoll) if preview { return poll.Poll.Question @@ -765,7 +825,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return strings.Join(rows, "\n") } case client.TypeMessageChatSetMessageAutoDeleteTime: - ttl, _ := message.Content.(*client.MessageChatSetMessageAutoDeleteTime) + ttl, _ := content.(*client.MessageChatSetMessageAutoDeleteTime) name := c.formatContact(ttl.FromUserId) if name == "" { if ttl.MessageAutoDeleteTime == 0 { @@ -782,7 +842,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { } } - return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType()) + return fmt.Sprintf("unknown message (%s)", content.MessageContentType()) } func (c *Client) contentToFile(content client.MessageContent) (*client.File, *client.File) { @@ -856,7 +916,7 @@ func (c *Client) countCharsInLines(lines *[]string) (count int) { return } -func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) { +func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string) (string, *gateway.Reply) { isPM, err := c.IsPM(message.ChatId) if err != nil { log.Errorf("Could not determine if chat is PM: %v", err) @@ -865,7 +925,6 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, // with carbons, hide for all messages in PM and only for outgoing in group chats hideSender := isCarbonsEnabled && (message.IsOutgoing || isPM) - var replyStart, replyEnd int prefix := []string{} // message direction var directionChar string @@ -894,21 +953,34 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, prefix = append(prefix, sender) } } + // reply to - if message.ReplyTo != nil && message.ReplyTo.MessageReplyToType() == client.TypeMessageReplyToMessage { - replyTo, _ := message.ReplyTo.(*client.MessageReplyToMessage) + preview := true + reply, tgReply := c.getMessageReply(message, preview, false) + + if tgReply != nil { + var replyStart, replyEnd int + if len(prefix) > 0 { replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) } - replyLine := "reply: " + c.formatMessage(message.ChatId, replyTo.MessageId, true, replyMsg) + + replyLine := "reply: " + c.formatMessageContent(preview, tgReply) prefix = append(prefix, replyLine) + replyEnd = replyStart + utf8.RuneCountInString(replyLine) if len(prefix) > 0 { replyEnd += len(messageHeaderSeparator) } + + if reply != nil { + reply.Start = uint64(replyStart) + reply.End = uint64(replyEnd) + } } + if message.ForwardInfo != nil { - prefix = append(prefix, "fwd: "+c.formatForward(message.ForwardInfo)) + prefix = append(prefix, "fwd: "+c.formatOrigin(message.ForwardInfo.Origin)) } // preview if previewString != "" { @@ -919,7 +991,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, prefix = append(prefix, "file: "+fileString) } - return strings.Join(prefix, messageHeaderSeparator), replyStart, replyEnd + return strings.Join(prefix, messageHeaderSeparator), reply } func (c *Client) ensureDownloadFile(file *client.File) *client.File { @@ -944,8 +1016,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { jids := c.getCarbonFullJids(isCarbon, "") var text, oob, auxText string - - reply, replyMsg := c.getMessageReply(message) + var reply *gateway.Reply + var replyObtained bool content := message.Content if content != nil && content.MessageContentType() == client.TypeMessageChatChangePhoto { @@ -981,12 +1053,10 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } else if !c.Session.RawMessages { var newText strings.Builder - prefix, replyStart, replyEnd := c.messageToPrefix(message, previewName, fileName, replyMsg) + prefix, prefixReply := c.messageToPrefix(message, previewName, fileName) + reply = prefixReply + replyObtained = true newText.WriteString(prefix) - if reply != nil { - reply.Start = uint64(replyStart) - reply.End = uint64(replyEnd) - } if text != "" { // \n if it is groupchat and message is not empty @@ -1004,6 +1074,9 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } } } + if !replyObtained { + reply, _ = c.getMessageReply(message, false, true) + } // mark message as read c.client.ViewMessages(&client.ViewMessagesRequest{ diff --git a/telegram/utils_test.go b/telegram/utils_test.go index 36b535c..534596f 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -436,15 +436,12 @@ func TestMessageToPrefix1(t *testing.T) { }, }, } - prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", nil) + prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "") if prefix != "➡ 42 | fwd: ziz" { t.Errorf("Wrong prefix: %v", prefix) } - if replyStart != 0 { - t.Errorf("Wrong replyStart: %v", replyStart) - } - if replyEnd != 0 { - t.Errorf("Wrong replyEnd: %v", replyEnd) + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) } } @@ -457,15 +454,12 @@ func TestMessageToPrefix2(t *testing.T) { }, }, } - prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", nil) + prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "") if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" { t.Errorf("Wrong prefix: %v", prefix) } - if replyStart != 0 { - t.Errorf("Wrong replyStart: %v", replyStart) - } - if replyEnd != 0 { - t.Errorf("Wrong replyEnd: %v", replyEnd) + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) } } @@ -478,15 +472,12 @@ func TestMessageToPrefix3(t *testing.T) { }, }, } - prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", nil) + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg") if prefix != "< 56 | fwd: (zuz) | file: a.jpg" { t.Errorf("Wrong prefix: %v", prefix) } - if replyStart != 0 { - t.Errorf("Wrong replyStart: %v", replyStart) - } - if replyEnd != 0 { - t.Errorf("Wrong replyEnd: %v", replyEnd) + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) } } @@ -495,15 +486,12 @@ func TestMessageToPrefix4(t *testing.T) { Id: 23, IsOutgoing: true, } - prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", nil) + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") if prefix != "> 23" { t.Errorf("Wrong prefix: %v", prefix) } - if replyStart != 0 { - t.Errorf("Wrong replyStart: %v", replyStart) - } - if replyEnd != 0 { - t.Errorf("Wrong replyEnd: %v", replyEnd) + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) } } @@ -516,43 +504,62 @@ func TestMessageToPrefix5(t *testing.T) { }, }, } - prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg", nil) + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg") if prefix != "< 560 | fwd: (zyz) | preview: h.jpg | file: a.jpg" { t.Errorf("Wrong prefix: %v", prefix) } - if replyStart != 0 { - t.Errorf("Wrong replyStart: %v", replyStart) - } - if replyEnd != 0 { - t.Errorf("Wrong replyEnd: %v", replyEnd) + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) } } func TestMessageToPrefix6(t *testing.T) { message := client.Message{ Id: 23, + ChatId: 25, IsOutgoing: true, ReplyTo: &client.MessageReplyToMessage{ - MessageId: 42, - }, - } - reply := client.Message{ - Id: 42, - Content: &client.MessageText{ - Text: &client.FormattedText{ - Text: "tist", + ChatId: 41, + Quote: &client.FormattedText{ + Text: "tist\nuz\niz", + }, + Origin: &client.MessageOriginHiddenUser{ + SenderName: "ziz", }, }, } - prefix, replyStart, replyEnd := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", &reply) - if prefix != "> 23 | reply: 42 | | tist" { + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") + if prefix != "> 23 | reply: ziz @ unknown contact: TDlib instance is offline | tist uz iz" { t.Errorf("Wrong prefix: %v", prefix) } - if replyStart != 4 { - t.Errorf("Wrong replyStart: %v", replyStart) + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) } - if replyEnd != 26 { - t.Errorf("Wrong replyEnd: %v", replyEnd) +} + +func TestMessageToPrefix7(t *testing.T) { + message := client.Message{ + Id: 23, + ChatId: 42, + IsOutgoing: true, + ReplyTo: &client.MessageReplyToMessage{ + ChatId: 41, + Content: &client.MessageText{ + Text: &client.FormattedText{ + Text: "tist", + }, + }, + Origin: &client.MessageOriginChannel{ + AuthorSignature: "zaz", + }, + }, + } + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") + if prefix != "> 23 | reply: (zaz) @ unknown contact: TDlib instance is offline | tist" { + t.Errorf("Wrong prefix: %v", prefix) + } + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) } }