From 67b8ad57f0aab1a033cbd8bcc8e75cf23342f5f9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 29 Oct 2023 08:47:35 -0400 Subject: [PATCH 01/54] Fix reply length for hrunicode messages --- Makefile | 2 +- telegabber.go | 2 +- telegram/utils.go | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 5c001af..99ef5c3 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.8.2" +VERSION := "v1.8.3" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index a1efd12..e316aa6 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.8.2" +var version string = "1.8.3" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index 47a851a..6edea31 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -16,6 +16,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "dev.narayana.im/narayana/telegabber/telegram/cache" "dev.narayana.im/narayana/telegabber/telegram/formatter" @@ -42,7 +43,7 @@ var spaceRegex = regexp.MustCompile(`\s+`) var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" -const messageHeaderSeparator string = " | " +const messageHeaderSeparator string = " | " // no hrunicode allowed here yet // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { @@ -845,7 +846,7 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl func (c *Client) countCharsInLines(lines *[]string) (count int) { for _, line := range *lines { - count += len(line) + count += utf8.RuneCountInString(line) } return } @@ -895,7 +896,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } replyLine := "reply: " + c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg) prefix = append(prefix, replyLine) - replyEnd = replyStart + len(replyLine) + replyEnd = replyStart + utf8.RuneCountInString(replyLine) if len(prefix) > 0 { replyEnd += len(messageHeaderSeparator) } From 576acba0d18717da5abc5a232cf92c23c9b8a550 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 11 Nov 2023 16:10:23 -0500 Subject: [PATCH 02/54] Migrate to TDLib 1.8.21 --- Makefile | 4 ++-- go.mod | 2 +- go.sum | 2 ++ telegabber.go | 25 ++++++++++++++++++++++++- telegram/client.go | 23 ----------------------- telegram/commands.go | 10 +++++----- telegram/utils.go | 44 +++++++++++++++++++++++++------------------- 7 files changed, 59 insertions(+), 51 deletions(-) diff --git a/Makefile b/Makefile index 99ef5c3..4d1a263 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ .PHONY: all test COMMIT := $(shell git rev-parse --short HEAD) -TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.8.3" +TD_COMMIT := "3870c29b158b75ca5e48e0eebd6b5c3a7994a000" +VERSION := "v1.9.0-dev" MAKEOPTS := "-j4" all: diff --git a/go.mod b/go.mod index db7c380..50e753d 100644 --- a/go.mod +++ b/go.mod @@ -34,4 +34,4 @@ require ( ) replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f -replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615 +replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268 diff --git a/go.sum b/go.sum index 2565582..6fd2f3e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615 h1:RRUZJSro+k8FkazNx7QEYLVoO4wZtchvsd0Y2RBWjeU= dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= +dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268 h1:NCbc2bYuUGQsb/3z5SCIia3N34Ktwq3FwaUAfgF/WEU= +dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/telegabber.go b/telegabber.go index e316aa6..8db6077 100644 --- a/telegabber.go +++ b/telegabber.go @@ -12,10 +12,11 @@ import ( "dev.narayana.im/narayana/telegabber/xmpp" log "github.com/sirupsen/logrus" + "github.com/zelenin/go-tdlib/client" goxmpp "gosrc.io/xmpp" ) -var version string = "1.8.3" +var version string = "1.9.0-dev" var commit string var sm *goxmpp.StreamManager @@ -60,6 +61,9 @@ func main() { log.Fatal(err) } + client.SetLogVerbosityLevel(&client.SetLogVerbosityLevelRequest{ + NewVerbosityLevel: stringToTdlibLogConstant(config.Telegram.Loglevel), + }) SetLogrusLevel(config.XMPP.Loglevel) log.Infof("Starting telegabber version %v", version) @@ -89,6 +93,25 @@ func main() { } } +var tdlibLogConstants = map[string]int32{ + ":fatal": 0, + ":error": 1, + ":warn": 2, + ":info": 3, + ":debug": 4, + ":verbose": 5, + ":all": 1023, +} + +func stringToTdlibLogConstant(c string) int32 { + level, ok := tdlibLogConstants[c] + if !ok { + level = 0 + } + + return level +} + func exit() { xmpp.Close(component) close(cleanupDone) diff --git a/telegram/client.go b/telegram/client.go index 6f6d719..49fc1ef 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -16,25 +16,6 @@ import ( "gosrc.io/xmpp" ) -var logConstants = map[string]int32{ - ":fatal": 0, - ":error": 1, - ":warn": 2, - ":info": 3, - ":debug": 4, - ":verbose": 5, - ":all": 1023, -} - -func stringToLogConstant(c string) int32 { - level, ok := logConstants[c] - if !ok { - level = 0 - } - - return level -} - // DelayedStatus describes an online status expiring on timeout type DelayedStatus struct { TimestampOnline int64 @@ -83,10 +64,6 @@ type clientLocks struct { func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component, session *persistence.Session) (*Client, error) { var options []client.Option - options = append(options, client.WithLogVerbosity(&client.SetLogVerbosityLevelRequest{ - NewVerbosityLevel: stringToLogConstant(conf.Loglevel), - })) - if conf.Tdlib.Client.CatchTimeout != 0 { options = append(options, client.WithCatchTimeout( time.Duration(conf.Tdlib.Client.CatchTimeout)*time.Second, diff --git a/telegram/commands.go b/telegram/commands.go index 87fff72..48c3615 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -422,7 +422,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string text := rawCmdArguments(cmdline, 1) _, err = c.client.ReportChat(&client.ReportChatRequest{ ChatId: contact.Id, - Reason: &client.ChatReportReasonCustom{}, + Reason: &client.ReportReasonCustom{}, Text: text, }) if err != nil { @@ -710,18 +710,18 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // blacklists current user case "block": - _, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{ + _, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{ SenderId: &client.MessageSenderUser{UserId: chatID}, - IsBlocked: true, + BlockList: &client.BlockListMain{}, }) if err != nil { return err.Error(), true } // unblacklists current user case "unblock": - _, err := c.client.ToggleMessageSenderIsBlocked(&client.ToggleMessageSenderIsBlockedRequest{ + _, err := c.client.SetMessageSenderBlockList(&client.SetMessageSenderBlockListRequest{ SenderId: &client.MessageSenderUser{UserId: chatID}, - IsBlocked: false, + BlockList: nil, }) if err != nil { return err.Error(), true diff --git a/telegram/utils.go b/telegram/utils.go index 6edea31..b22f156 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -343,20 +343,28 @@ func (c *Client) formatSender(message *client.Message) string { } func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyMsg *client.Message) { - if message.ReplyToMessageId != 0 { + 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 err error replyMsg, err = c.client.GetMessage(&client.GetMessageRequest{ ChatId: message.ChatId, - MessageId: message.ReplyToMessageId, + MessageId: replyTo.MessageId, }) if err != nil { log.Errorf("", err.Error()) return } - replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, message.ReplyToMessageId) + replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, replyTo.MessageId) if err != nil { - replyId = strconv.FormatInt(message.ReplyToMessageId, 10) + replyId = strconv.FormatInt(replyTo.MessageId, 10) } reply = &gateway.Reply{ Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), @@ -417,30 +425,27 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess } func (c *Client) formatForward(fwd *client.MessageForwardInfo) string { - switch fwd.Origin.MessageForwardOriginType() { - case client.TypeMessageForwardOriginUser: - originUser := fwd.Origin.(*client.MessageForwardOriginUser) + switch fwd.Origin.MessageOriginType() { + case client.TypeMessageOriginUser: + originUser := fwd.Origin.(*client.MessageOriginUser) return c.formatContact(originUser.SenderUserId) - case client.TypeMessageForwardOriginChat: - originChat := fwd.Origin.(*client.MessageForwardOriginChat) + case client.TypeMessageOriginChat: + originChat := fwd.Origin.(*client.MessageOriginChat) var signature string if originChat.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) } return c.formatContact(originChat.SenderChatId) + signature - case client.TypeMessageForwardOriginHiddenUser: - originUser := fwd.Origin.(*client.MessageForwardOriginHiddenUser) + case client.TypeMessageOriginHiddenUser: + originUser := fwd.Origin.(*client.MessageOriginHiddenUser) return originUser.SenderName - case client.TypeMessageForwardOriginChannel: - channel := fwd.Origin.(*client.MessageForwardOriginChannel) + case client.TypeMessageOriginChannel: + channel := fwd.Origin.(*client.MessageOriginChannel) var signature string if channel.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) } return c.formatContact(channel.ChatId) + signature - case client.TypeMessageForwardOriginMessageImport: - originImport := fwd.Origin.(*client.MessageForwardOriginMessageImport) - return originImport.SenderName } return "Unknown forward type" } @@ -890,11 +895,12 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } } // reply to - if message.ReplyToMessageId != 0 { + if message.ReplyTo != nil && message.ReplyTo.MessageReplyToType() == client.TypeMessageReplyToMessage { + replyTo, _ := message.ReplyTo.(*client.MessageReplyToMessage) if len(prefix) > 0 { replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) } - replyLine := "reply: " + c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg) + replyLine := "reply: " + c.formatMessage(message.ChatId, replyTo.MessageId, true, replyMsg) prefix = append(prefix, replyLine) replyEnd = replyStart + utf8.RuneCountInString(replyLine) if len(prefix) > 0 { @@ -1116,7 +1122,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ ChatId: chatID, - ReplyToMessageId: reply, + ReplyTo: &client.InputMessageReplyToMessage{MessageId: reply}, InputMessageContent: content, }) if err != nil { From 6bd837911431ef68d23de1bcbb75893edd39a32b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 15 Nov 2023 19:38:45 -0500 Subject: [PATCH 03/54] Support blockquotes in formatter --- telegram/formatter/formatter.go | 233 +++++++++++++++++++++------ telegram/formatter/formatter_test.go | 198 +++++++++++++++++------ telegram/utils.go | 20 +-- 3 files changed, 350 insertions(+), 101 deletions(-) diff --git a/telegram/formatter/formatter.go b/telegram/formatter/formatter.go index 740fa09..9403198 100644 --- a/telegram/formatter/formatter.go +++ b/telegram/formatter/formatter.go @@ -8,15 +8,29 @@ import ( "github.com/zelenin/go-tdlib/client" ) -// Insertion is a piece of text in given position -type Insertion struct { +type insertionType int +const ( + insertionOpening insertionType = iota + insertionClosing + insertionUnpaired +) + +type MarkupModeType int +const ( + MarkupModeXEP0393 MarkupModeType = iota + MarkupModeMarkdown +) + +// insertion is a piece of text in given position +type insertion struct { Offset int32 Runes []rune + Type insertionType } -// InsertionStack contains the sequence of insertions +// insertionStack contains the sequence of insertions // from the start or from the end -type InsertionStack []*Insertion +type insertionStack []*insertion var boldRunesMarkdown = []rune("**") var boldRunesXEP0393 = []rune("*") @@ -26,11 +40,16 @@ var strikeRunesXEP0393 = []rune("~") var codeRunes = []rune("`") var preRuneStart = []rune("```\n") var preRuneEnd = []rune("\n```") +var quoteRunes = []rune("> ") +var newlineRunes = []rune("\n") +var doubleNewlineRunes = []rune("\n\n") +var newlineCode = rune(0x0000000a) +var bmpCeil = rune(0x0000ffff) // rebalance pumps all the values until the given offset to current stack (growing // from start) from given stack (growing from end); should be called // before any insertions to the current stack at the given offset -func (s InsertionStack) rebalance(s2 InsertionStack, offset int32) (InsertionStack, InsertionStack) { +func (s insertionStack) rebalance(s2 insertionStack, offset int32) (insertionStack, insertionStack) { for len(s2) > 0 && s2[len(s2)-1].Offset <= offset { s = append(s, s2[len(s2)-1]) s2 = s2[:len(s2)-1] @@ -41,10 +60,10 @@ func (s InsertionStack) rebalance(s2 InsertionStack, offset int32) (InsertionSta // NewIterator is a second order function that sequentially scans and returns // stack elements; starts returning nil when elements are ended -func (s InsertionStack) NewIterator() func() *Insertion { +func (s insertionStack) NewIterator() func() *insertion { i := -1 - return func() *Insertion { + return func() *insertion { i++ if i < len(s) { return s[i] @@ -120,21 +139,10 @@ func MergeAdjacentEntities(entities []*client.TextEntity) []*client.TextEntity { } // ClaspDirectives to the following span as required by XEP-0393 -func ClaspDirectives(text string, entities []*client.TextEntity) []*client.TextEntity { +func ClaspDirectives(doubledRunes []rune, entities []*client.TextEntity) []*client.TextEntity { alignedEntities := make([]*client.TextEntity, len(entities)) copy(alignedEntities, entities) - // transform the source text into a form with uniform runes and code points, - // by duplicating the Basic Multilingual Plane - doubledRunes := make([]rune, 0, len(text)*2) - - for _, cp := range text { - if cp > 0x0000ffff { - doubledRunes = append(doubledRunes, cp, cp) - } else { - doubledRunes = append(doubledRunes, cp) - } - } for i, entity := range alignedEntities { var dirty bool endOffset := entity.Offset + entity.Length @@ -167,18 +175,89 @@ func ClaspDirectives(text string, entities []*client.TextEntity) []*client.TextE return alignedEntities } -func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) (*Insertion, *Insertion) { - return &Insertion{ +func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) []*insertion { + return []*insertion{ + &insertion{ Offset: entity.Offset, Runes: lbrace, - }, &Insertion{ + Type: insertionOpening, + }, + &insertion{ Offset: entity.Offset + entity.Length, Runes: rbrace, - } + Type: insertionClosing, + }, + } } -// EntityToMarkdown generates the wrapping Markdown tags -func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) { +func quotePrependNewlines(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion { + if len(doubledRunes) == 0 { + return []*insertion{} + } + + startRunes := []rune("\n> ") + if entity.Offset == 0 || doubledRunes[entity.Offset-1] == newlineCode { + startRunes = quoteRunes + } + insertions := []*insertion{ + &insertion{ + Offset: entity.Offset, + Runes: startRunes, + Type: insertionUnpaired, + }, + } + + entityEnd := entity.Offset + entity.Length + entityEndInt := int(entityEnd) + + var wasNewline bool + // last newline is omitted, there's no need to put quote mark after the quote + for i := entity.Offset; i < entityEnd-1; i++ { + isNewline := doubledRunes[i] == newlineCode + if (isNewline && markupMode == MarkupModeXEP0393) || (wasNewline && isNewline && markupMode == MarkupModeMarkdown) { + insertions = append(insertions, &insertion{ + Offset: i+1, + Runes: quoteRunes, + Type: insertionUnpaired, + }) + } + + if isNewline { + wasNewline = true + } else { + wasNewline = false + } + } + + var rbrace []rune + if len(doubledRunes) > entityEndInt { + if doubledRunes[entityEnd] == newlineCode { + if markupMode == MarkupModeMarkdown && len(doubledRunes) > entityEndInt+1 && doubledRunes[entityEndInt+1] != newlineCode { + rbrace = newlineRunes + } + } else { + if markupMode == MarkupModeMarkdown { + rbrace = doubleNewlineRunes + } else { + rbrace = newlineRunes + } + } + } + insertions = append(insertions, &insertion{ + Offset: entityEnd, + Runes: rbrace, + Type: insertionClosing, + }) + + return insertions +} + +// entityToMarkdown generates the wrapping Markdown tags +func entityToMarkdown(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion { + if entity == nil || entity.Type == nil { + return []*insertion{} + } + switch entity.Type.TextEntityTypeType() { case client.TypeTextEntityTypeBold: return markupBraces(entity, boldRunesMarkdown, boldRunesMarkdown) @@ -193,18 +272,20 @@ func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) { case client.TypeTextEntityTypePreCode: preCode, _ := entity.Type.(*client.TextEntityTypePreCode) return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes) + case client.TypeTextEntityTypeBlockQuote: + return quotePrependNewlines(entity, doubledRunes, MarkupModeMarkdown) case client.TypeTextEntityTypeTextUrl: textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl) return markupBraces(entity, []rune("["), []rune("]("+textURL.Url+")")) } - return nil, nil + return []*insertion{} } -// EntityToXEP0393 generates the wrapping XEP-0393 tags -func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) { +// entityToXEP0393 generates the wrapping XEP-0393 tags +func entityToXEP0393(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion { if entity == nil || entity.Type == nil { - return nil, nil + return []*insertion{} } switch entity.Type.TextEntityTypeType() { @@ -221,29 +302,55 @@ func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) { case client.TypeTextEntityTypePreCode: preCode, _ := entity.Type.(*client.TextEntityTypePreCode) return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes) + case client.TypeTextEntityTypeBlockQuote: + return quotePrependNewlines(entity, doubledRunes, MarkupModeXEP0393) case client.TypeTextEntityTypeTextUrl: textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl) // non-standard, Pidgin-specific return markupBraces(entity, []rune{}, []rune(" <"+textURL.Url+">")) } - return nil, nil + return []*insertion{} +} + +// transform the source text into a form with uniform runes and code points, +// by duplicating anything beyond the Basic Multilingual Plane +func textToDoubledRunes(text string) []rune { + doubledRunes := make([]rune, 0, len(text)*2) + for _, cp := range text { + if cp > bmpCeil { + doubledRunes = append(doubledRunes, cp, cp) + } else { + doubledRunes = append(doubledRunes, cp) + } + } + + return doubledRunes } // Format traverses an already sorted list of entities and wraps the text in a markup func Format( sourceText string, entities []*client.TextEntity, - entityToMarkup func(*client.TextEntity) (*Insertion, *Insertion), + markupMode MarkupModeType, ) string { if len(entities) == 0 { return sourceText } - mergedEntities := SortEntities(ClaspDirectives(sourceText, MergeAdjacentEntities(SortEntities(entities)))) + var entityToMarkup func(*client.TextEntity, []rune, MarkupModeType) []*insertion + if markupMode == MarkupModeXEP0393 { + entityToMarkup = entityToXEP0393 + } else { + entityToMarkup = entityToMarkdown + } - startStack := make(InsertionStack, 0, len(sourceText)) - endStack := make(InsertionStack, 0, len(sourceText)) + doubledRunes := textToDoubledRunes(sourceText) + + mergedEntities := SortEntities(ClaspDirectives(doubledRunes, MergeAdjacentEntities(SortEntities(entities)))) + + startStack := make(insertionStack, 0, len(sourceText)) + endStack := make(insertionStack, 0, len(sourceText)) // convert entities to a stack of brackets var maxEndOffset int32 @@ -260,36 +367,70 @@ func Format( startStack, endStack = startStack.rebalance(endStack, entity.Offset) - startInsertion, endInsertion := entityToMarkup(entity) - if startInsertion != nil { - startStack = append(startStack, startInsertion) + insertions := entityToMarkup(entity, doubledRunes, markupMode) + if len(insertions) > 1 { + startStack = append(startStack, insertions[0:len(insertions)-1]...) } - if endInsertion != nil { - endStack = append(endStack, endInsertion) + if len(insertions) > 0 { + endStack = append(endStack, insertions[len(insertions)-1]) } } // flush the closing brackets that still remain in endStack startStack, endStack = startStack.rebalance(endStack, maxEndOffset) + // sort unpaired insertions + sort.SliceStable(startStack, func(i int, j int) bool { + ins1 := startStack[i] + ins2 := startStack[j] + if ins1.Type == insertionUnpaired && ins2.Type == insertionUnpaired { + return ins1.Offset < ins2.Offset + } + if ins1.Type == insertionUnpaired { + if ins1.Offset == ins2.Offset { + if ins2.Type == insertionOpening { // > ** + return true + } else if ins2.Type == insertionClosing { // **> + return false + } + } else { + return ins1.Offset < ins2.Offset + } + } + if ins2.Type == insertionUnpaired { + if ins1.Offset == ins2.Offset { + if ins1.Type == insertionOpening { // > ** + return false + } else if ins1.Type == insertionClosing { // **> + return true + } + } else { + return ins1.Offset < ins2.Offset + } + } + return false + }) // merge brackets into text markupRunes := make([]rune, 0, len(sourceText)) nextInsertion := startStack.NewIterator() insertion := nextInsertion() - var runeI int32 + var skipNext bool - for _, cp := range sourceText { - for insertion != nil && insertion.Offset <= runeI { + for i, cp := range doubledRunes { + if skipNext { + skipNext = false + continue + } + + for insertion != nil && int(insertion.Offset) <= i { markupRunes = append(markupRunes, insertion.Runes...) insertion = nextInsertion() } markupRunes = append(markupRunes, cp) // skip two UTF-16 code units (not points actually!) if needed - if cp > 0x0000ffff { - runeI += 2 - } else { - runeI++ + if cp > bmpCeil { + skipNext = true } } for insertion != nil { diff --git a/telegram/formatter/formatter_test.go b/telegram/formatter/formatter_test.go index e4bdd23..187d486 100644 --- a/telegram/formatter/formatter_test.go +++ b/telegram/formatter/formatter_test.go @@ -7,7 +7,7 @@ import ( ) func TestNoFormatting(t *testing.T) { - markup := Format("abc\ndef", []*client.TextEntity{}, EntityToMarkdown) + markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeMarkdown) if markup != "abc\ndef" { t.Errorf("No formatting expected, but: %v", markup) } @@ -20,7 +20,7 @@ func TestFormattingSimple(t *testing.T) { Length: 4, Type: &client.TextEntityTypeBold{}, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "👙**🐧🐖**" { t.Errorf("Wrong simple formatting: %v", markup) } @@ -40,7 +40,7 @@ func TestFormattingAdjacent(t *testing.T) { Url: "https://narayana.im/", }, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "a👙_🐧_[🐖](https://narayana.im/)" { t.Errorf("Wrong adjacent formatting: %v", markup) } @@ -63,18 +63,18 @@ func TestFormattingAdjacentAndNested(t *testing.T) { Length: 2, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "```\n**👙**🐧\n```_🐖_" { t.Errorf("Wrong adjacent&nested formatting: %v", markup) } } func TestRebalanceTwoZero(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{} + s2 := insertionStack{} s1, s2 = s1.rebalance(s2, 7) if !(len(s1) == 2 && len(s2) == 0 && s1[0].Offset == 7 && s1[1].Offset == 8) { t.Errorf("Wrong rebalance 2–0: %#v %#v", s1, s2) @@ -82,13 +82,13 @@ func TestRebalanceTwoZero(t *testing.T) { } func TestRebalanceNeeded(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{ - &Insertion{Offset: 10}, - &Insertion{Offset: 9}, + s2 := insertionStack{ + &insertion{Offset: 10}, + &insertion{Offset: 9}, } s1, s2 = s1.rebalance(s2, 9) if !(len(s1) == 3 && len(s2) == 1 && @@ -99,13 +99,13 @@ func TestRebalanceNeeded(t *testing.T) { } func TestRebalanceNotNeeded(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{ - &Insertion{Offset: 10}, - &Insertion{Offset: 9}, + s2 := insertionStack{ + &insertion{Offset: 10}, + &insertion{Offset: 9}, } s1, s2 = s1.rebalance(s2, 8) if !(len(s1) == 2 && len(s2) == 2 && @@ -116,13 +116,13 @@ func TestRebalanceNotNeeded(t *testing.T) { } func TestRebalanceLate(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{ - &Insertion{Offset: 10}, - &Insertion{Offset: 9}, + s2 := insertionStack{ + &insertion{Offset: 10}, + &insertion{Offset: 9}, } s1, s2 = s1.rebalance(s2, 10) if !(len(s1) == 4 && len(s2) == 0 && @@ -133,7 +133,7 @@ func TestRebalanceLate(t *testing.T) { } func TestIteratorEmpty(t *testing.T) { - s := InsertionStack{} + s := insertionStack{} g := s.NewIterator() v := g() if v != nil { @@ -142,9 +142,9 @@ func TestIteratorEmpty(t *testing.T) { } func TestIterator(t *testing.T) { - s := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } g := s.NewIterator() v := g() @@ -208,7 +208,7 @@ func TestSortEmpty(t *testing.T) { } func TestNoFormattingXEP0393(t *testing.T) { - markup := Format("abc\ndef", []*client.TextEntity{}, EntityToXEP0393) + markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeXEP0393) if markup != "abc\ndef" { t.Errorf("No formatting expected, but: %v", markup) } @@ -221,7 +221,7 @@ func TestFormattingXEP0393Simple(t *testing.T) { Length: 4, Type: &client.TextEntityTypeBold{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "👙*🐧🐖*" { t.Errorf("Wrong simple formatting: %v", markup) } @@ -241,7 +241,7 @@ func TestFormattingXEP0393Adjacent(t *testing.T) { Url: "https://narayana.im/", }, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "a👙_🐧_🐖 " { t.Errorf("Wrong adjacent formatting: %v", markup) } @@ -264,7 +264,7 @@ func TestFormattingXEP0393AdjacentAndNested(t *testing.T) { Length: 2, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "```\n*👙*🐧\n```_🐖_" { t.Errorf("Wrong adjacent&nested formatting: %v", markup) } @@ -287,7 +287,7 @@ func TestFormattingXEP0393AdjacentItalicBoldItalic(t *testing.T) { Length: 69, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "_раса двуногих крысолюдей, *которую так редко замечают, что многие отрицают само их существование*_" { t.Errorf("Wrong adjacent italic/bold-italic formatting: %v", markup) } @@ -315,7 +315,7 @@ func TestFormattingXEP0393MultipleAdjacent(t *testing.T) { Length: 1, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "a*bcd*_e_" { t.Errorf("Wrong multiple adjacent formatting: %v", markup) } @@ -343,7 +343,7 @@ func TestFormattingXEP0393Intersecting(t *testing.T) { Length: 1, Type: &client.TextEntityTypeBold{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "a*b*_*cd*e_" { t.Errorf("Wrong intersecting formatting: %v", markup) } @@ -361,7 +361,7 @@ func TestFormattingXEP0393InlineCode(t *testing.T) { Length: 25, Type: &client.TextEntityTypePre{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "Is `Gajim` a thing?\n\n```\necho 'Hello'\necho 'world'\n```\n\nhruck(" { t.Errorf("Wrong intersecting formatting: %v", markup) } @@ -374,7 +374,7 @@ func TestFormattingMarkdownStrikethrough(t *testing.T) { Length: 3, Type: &client.TextEntityTypeStrikethrough{}, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "Everyone ~~dis~~likes cake." { t.Errorf("Wrong strikethrough formatting: %v", markup) } @@ -387,14 +387,14 @@ func TestFormattingXEP0393Strikethrough(t *testing.T) { Length: 3, Type: &client.TextEntityTypeStrikethrough{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "Everyone ~dis~likes cake." { t.Errorf("Wrong strikethrough formatting: %v", markup) } } func TestClaspLeft(t *testing.T) { - text := "a b c" + text := textToDoubledRunes("a b c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -409,7 +409,7 @@ func TestClaspLeft(t *testing.T) { } func TestClaspBoth(t *testing.T) { - text := "a b c" + text := textToDoubledRunes("a b c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -424,7 +424,7 @@ func TestClaspBoth(t *testing.T) { } func TestClaspNotNeeded(t *testing.T) { - text := " abc " + text := textToDoubledRunes(" abc ") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -439,7 +439,7 @@ func TestClaspNotNeeded(t *testing.T) { } func TestClaspNested(t *testing.T) { - text := "a b c" + text := textToDoubledRunes("a b c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -459,7 +459,7 @@ func TestClaspNested(t *testing.T) { } func TestClaspEmoji(t *testing.T) { - text := "a 🐖 c" + text := textToDoubledRunes("a 🐖 c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -472,3 +472,111 @@ func TestClaspEmoji(t *testing.T) { t.Errorf("Wrong claspemoji: %#v", entities) } } + +func TestNoNewlineBlockquoteXEP0393(t *testing.T) { + markup := Format("yes it can i think", []*client.TextEntity{ + &client.TextEntity{ + Offset: 4, + Length: 6, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeXEP0393) + if markup != "yes \n> it can\n i think" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestNoNewlineBlockquoteMarkdown(t *testing.T) { + markup := Format("yes it can i think", []*client.TextEntity{ + &client.TextEntity{ + Offset: 4, + Length: 6, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeMarkdown) + if markup != "yes \n> it can\n\n i think" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMultilineBlockquoteXEP0393(t *testing.T) { + markup := Format("hruck\npuck\n\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 17, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeXEP0393) + if markup != "> hruck\n> puck\n> \n> shuck\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMultilineBlockquoteMarkdown(t *testing.T) { + markup := Format("hruck\npuck\n\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 17, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeMarkdown) + if markup != "> hruck\npuck\n\n> shuck\n\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMixedBlockquoteXEP0393(t *testing.T) { + markup := Format("hruck\npuck\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBlockQuote{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBold{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 10, + Type: &client.TextEntityTypeItalic{}, + }, + &client.TextEntity{ + Offset: 7, + Length: 2, + Type: &client.TextEntityTypeStrikethrough{}, + }, + }, MarkupModeXEP0393) + if markup != "> *_hruck\n> p~uc~k_\n> shuck*\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMixedBlockquoteMarkdown(t *testing.T) { + markup := Format("hruck\npuck\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBlockQuote{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBold{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 10, + Type: &client.TextEntityTypeItalic{}, + }, + &client.TextEntity{ + Offset: 7, + Length: 2, + Type: &client.TextEntityTypeStrikethrough{}, + }, + }, MarkupModeMarkdown) + if markup != "> **_hruck\np~~uc~~k_\nshuck**\n\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} diff --git a/telegram/utils.go b/telegram/utils.go index b22f156..9370839 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -593,7 +593,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return "" } - markupFunction := c.getFormatter() + markupMode := c.getFormatter() switch message.Content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := message.Content.(*client.MessageSticker) @@ -646,7 +646,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( photo.Caption.Text, photo.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageAudio: @@ -657,7 +657,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( audio.Caption.Text, audio.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageVideo: @@ -668,7 +668,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( video.Caption.Text, video.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageDocument: @@ -679,7 +679,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( document.Caption.Text, document.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageText: @@ -690,7 +690,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( text.Text.Text, text.Text.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageVoiceNote: @@ -701,7 +701,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( voice.Caption.Text, voice.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageVideoNote: @@ -714,7 +714,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( animation.Caption.Text, animation.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageContact: @@ -1500,8 +1500,8 @@ func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content clie return !ok || oldHash != newHash } -func (c *Client) getFormatter() func(*client.TextEntity) (*formatter.Insertion, *formatter.Insertion) { - return formatter.EntityToXEP0393 +func (c *Client) getFormatter() formatter.MarkupModeType { + return formatter.MarkupModeXEP0393 } func (c *Client) usernamesToString(usernames []string) string { From dcb802358b8f7d8a72da04cce71b2dcc0f6a2fbc Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 16 Nov 2023 08:05:23 -0500 Subject: [PATCH 04/54] Fix tests --- telegabber_test.go | 19 +++++++++++++++++++ telegram/client_test.go | 19 ------------------- telegram/utils_test.go | 16 +++++++++------- 3 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 telegabber_test.go delete mode 100644 telegram/client_test.go diff --git a/telegabber_test.go b/telegabber_test.go new file mode 100644 index 0000000..459f333 --- /dev/null +++ b/telegabber_test.go @@ -0,0 +1,19 @@ +package main + +import ( + "testing" +) + +func TestTdlibLogInfo(t *testing.T) { + tdlibConstant := stringToTdlibLogConstant(":info") + if tdlibConstant != 3 { + t.Errorf("Wrong TDlib constant for info") + } +} + +func TestTdlibLogInvalid(t *testing.T) { + tdlibConstant := stringToTdlibLogConstant("ziz") + if tdlibConstant != 0 { + t.Errorf("Unknown strings should return fatal loglevel") + } +} diff --git a/telegram/client_test.go b/telegram/client_test.go deleted file mode 100644 index 4c757e1..0000000 --- a/telegram/client_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package telegram - -import ( - "testing" -) - -func TestLogInfo(t *testing.T) { - tdlibConstant := stringToLogConstant(":info") - if tdlibConstant != 3 { - t.Errorf("Wrong TDlib constant for info") - } -} - -func TestLogInvalid(t *testing.T) { - tdlibConstant := stringToLogConstant("ziz") - if tdlibConstant != 0 { - t.Errorf("Unknown strings should return fatal loglevel") - } -} diff --git a/telegram/utils_test.go b/telegram/utils_test.go index e54ddb5..47e30dc 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -431,7 +431,7 @@ func TestMessageToPrefix1(t *testing.T) { Id: 42, IsOutgoing: true, ForwardInfo: &client.MessageForwardInfo{ - Origin: &client.MessageForwardOriginHiddenUser{ + Origin: &client.MessageOriginHiddenUser{ SenderName: "ziz", }, }, @@ -452,7 +452,7 @@ func TestMessageToPrefix2(t *testing.T) { message := client.Message{ Id: 56, ForwardInfo: &client.MessageForwardInfo{ - Origin: &client.MessageForwardOriginChannel{ + Origin: &client.MessageOriginChannel{ AuthorSignature: "zaz", }, }, @@ -473,7 +473,7 @@ func TestMessageToPrefix3(t *testing.T) { message := client.Message{ Id: 56, ForwardInfo: &client.MessageForwardInfo{ - Origin: &client.MessageForwardOriginChannel{ + Origin: &client.MessageOriginChannel{ AuthorSignature: "zuz", }, }, @@ -511,7 +511,7 @@ func TestMessageToPrefix5(t *testing.T) { message := client.Message{ Id: 560, ForwardInfo: &client.MessageForwardInfo{ - Origin: &client.MessageForwardOriginChat{ + Origin: &client.MessageOriginChat{ AuthorSignature: "zyz", }, }, @@ -530,9 +530,11 @@ func TestMessageToPrefix5(t *testing.T) { func TestMessageToPrefix6(t *testing.T) { message := client.Message{ - Id: 23, - IsOutgoing: true, - ReplyToMessageId: 42, + Id: 23, + IsOutgoing: true, + ReplyTo: &client.MessageReplyToMessage{ + MessageId: 42, + }, } reply := client.Message{ Id: 42, From 705cfc1d496f96875da5c13209f0b78803183843 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 16 Nov 2023 08:06:21 -0500 Subject: [PATCH 05/54] gofmt --- telegram/formatter/formatter.go | 8 +++++--- telegram/utils_test.go | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/telegram/formatter/formatter.go b/telegram/formatter/formatter.go index 9403198..6da8256 100644 --- a/telegram/formatter/formatter.go +++ b/telegram/formatter/formatter.go @@ -9,6 +9,7 @@ import ( ) type insertionType int + const ( insertionOpening insertionType = iota insertionClosing @@ -16,6 +17,7 @@ const ( ) type MarkupModeType int + const ( MarkupModeXEP0393 MarkupModeType = iota MarkupModeMarkdown @@ -216,7 +218,7 @@ func quotePrependNewlines(entity *client.TextEntity, doubledRunes []rune, markup isNewline := doubledRunes[i] == newlineCode if (isNewline && markupMode == MarkupModeXEP0393) || (wasNewline && isNewline && markupMode == MarkupModeMarkdown) { insertions = append(insertions, &insertion{ - Offset: i+1, + Offset: i + 1, Runes: quoteRunes, Type: insertionUnpaired, }) @@ -388,7 +390,7 @@ func Format( if ins1.Offset == ins2.Offset { if ins2.Type == insertionOpening { // > ** return true - } else if ins2.Type == insertionClosing { // **> + } else if ins2.Type == insertionClosing { // **> return false } } else { @@ -399,7 +401,7 @@ func Format( if ins1.Offset == ins2.Offset { if ins1.Type == insertionOpening { // > ** return false - } else if ins1.Type == insertionClosing { // **> + } else if ins1.Type == insertionClosing { // **> return true } } else { diff --git a/telegram/utils_test.go b/telegram/utils_test.go index 47e30dc..36b535c 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -532,7 +532,7 @@ func TestMessageToPrefix6(t *testing.T) { message := client.Message{ Id: 23, IsOutgoing: true, - ReplyTo: &client.MessageReplyToMessage{ + ReplyTo: &client.MessageReplyToMessage{ MessageId: 42, }, } From f2807779aad0dd0d463d396d7ae7e2de48a83c3b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 16 Nov 2023 08:44:26 -0500 Subject: [PATCH 06/54] Fix ending braces for PreCode --- telegram/formatter/formatter.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telegram/formatter/formatter.go b/telegram/formatter/formatter.go index 6da8256..a8c94a0 100644 --- a/telegram/formatter/formatter.go +++ b/telegram/formatter/formatter.go @@ -40,8 +40,8 @@ var italicRunes = []rune("_") var strikeRunesMarkdown = []rune("~~") var strikeRunesXEP0393 = []rune("~") var codeRunes = []rune("`") -var preRuneStart = []rune("```\n") -var preRuneEnd = []rune("\n```") +var preRunesStart = []rune("```\n") +var preRunesEnd = []rune("\n```") var quoteRunes = []rune("> ") var newlineRunes = []rune("\n") var doubleNewlineRunes = []rune("\n\n") @@ -270,10 +270,10 @@ func entityToMarkdown(entity *client.TextEntity, doubledRunes []rune, markupMode case client.TypeTextEntityTypeCode: return markupBraces(entity, codeRunes, codeRunes) case client.TypeTextEntityTypePre: - return markupBraces(entity, preRuneStart, preRuneEnd) + return markupBraces(entity, preRunesStart, preRunesEnd) case client.TypeTextEntityTypePreCode: preCode, _ := entity.Type.(*client.TextEntityTypePreCode) - return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes) + return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), preRunesEnd) case client.TypeTextEntityTypeBlockQuote: return quotePrependNewlines(entity, doubledRunes, MarkupModeMarkdown) case client.TypeTextEntityTypeTextUrl: @@ -300,10 +300,10 @@ func entityToXEP0393(entity *client.TextEntity, doubledRunes []rune, markupMode case client.TypeTextEntityTypeCode: return markupBraces(entity, codeRunes, codeRunes) case client.TypeTextEntityTypePre: - return markupBraces(entity, preRuneStart, preRuneEnd) + return markupBraces(entity, preRunesStart, preRunesEnd) case client.TypeTextEntityTypePreCode: preCode, _ := entity.Type.(*client.TextEntityTypePreCode) - return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes) + return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), preRunesEnd) case client.TypeTextEntityTypeBlockQuote: return quotePrependNewlines(entity, doubledRunes, MarkupModeXEP0393) case client.TypeTextEntityTypeTextUrl: From 4532748c8458971151dfb6b535b11b2a3e17a372 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 10 Jan 2024 14:30:00 -0500 Subject: [PATCH 07/54] 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) } } From b40ccf4a4d1391ba1f67a159697fea859e2a92fd Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 24 Jan 2024 18:52:40 -0500 Subject: [PATCH 08/54] Fix presences sent with no resource --- telegram/commands.go | 8 ++------ telegram/connect.go | 11 +++-------- telegram/utils.go | 33 +++++++++++---------------------- xmpp/gateway/gateway.go | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index c4b5988..19fd655 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -185,12 +185,8 @@ func keyValueString(key, value string) string { } func (c *Client) unsubscribe(chatID int64) error { - return gateway.SendPresence( - c.xmpp, - c.jid, - gateway.SPFrom(strconv.FormatInt(chatID, 10)), - gateway.SPType("unsubscribed"), - ) + args := gateway.SimplePresence(chatID, "unsubscribed") + return c.sendPresence(args...) } func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { diff --git a/telegram/connect.go b/telegram/connect.go index b1b8b10..6c88cd1 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -2,7 +2,6 @@ package telegram import ( "github.com/pkg/errors" - "strconv" "time" "dev.narayana.im/narayana/telegabber/xmpp/gateway" @@ -159,7 +158,7 @@ func (c *Client) Connect(resource string) error { } gateway.SubscribeToTransport(c.xmpp, c.jid) - gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login)) + c.sendPresence(gateway.SPStatus("Logged in as: "+c.Session.Login)) }() return nil @@ -228,12 +227,8 @@ func (c *Client) Disconnect(resource string, quit bool) bool { // we're offline (unsubscribe if logout) for _, id := range c.cache.ChatsKeys() { - gateway.SendPresence( - c.xmpp, - c.jid, - gateway.SPFrom(strconv.FormatInt(id, 10)), - gateway.SPType("unavailable"), - ) + args := gateway.SimplePresence(id, "unavailable") + c.sendPresence(args...) } c.close() diff --git a/telegram/utils.go b/telegram/utils.go index b17d692..f0316f2 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -281,22 +281,17 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o c.cache.SetStatus(chatID, cacheShow, status) newArgs := []args.V{ - gateway.SPFrom(strconv.FormatInt(chatID, 10)), gateway.SPShow(show), gateway.SPStatus(status), gateway.SPPhoto(photo), - gateway.SPResource(gateway.Jid.Resource), gateway.SPImmed(gateway.SPImmed.Get(oldArgs)), } + newArgs = gateway.SPAppendFrom(newArgs, chatID) if presenceType != "" { newArgs = append(newArgs, gateway.SPType(presenceType)) } - return gateway.SendPresence( - c.xmpp, - c.jid, - newArgs..., - ) + return c.sendPresence(newArgs...) } func (c *Client) formatContact(chatID int64) string { @@ -1292,7 +1287,7 @@ func (c *Client) roster(resource string) { c.ProcessStatusUpdate(chat, "", "") } - gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login)) + c.sendPresence(gateway.SPStatus("Logged in as: "+c.Session.Login)) c.addResource(resource) } @@ -1393,9 +1388,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { // subscribe to a Telegram ID func (c *Client) subscribeToID(id int64, chat *client.Chat) { - var args []args.V - args = append(args, gateway.SPFrom(strconv.FormatInt(id, 10))) - args = append(args, gateway.SPType("subscribe")) + args := gateway.SimplePresence(id, "subscribe") if chat == nil { chat, _, _ = c.GetContactByID(id, nil) @@ -1406,11 +1399,11 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) { gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) } - gateway.SendPresence( - c.xmpp, - c.jid, - args..., - ) + c.sendPresence(args...) +} + +func (c *Client) sendPresence(args ...args.V) error { + return gateway.SendPresence(c.xmpp, c.jid, args...) } func (c *Client) prepareDiskSpace(size uint64) { @@ -1459,9 +1452,9 @@ func (c *Client) UpdateChatNicknames() { chat, ok := c.cache.GetChat(id) if ok { newArgs := []args.V{ - gateway.SPFrom(strconv.FormatInt(id, 10)), gateway.SPNickname(chat.Title), } + newArgs = gateway.SPAppendFrom(newArgs, id) cachedStatus, ok := c.cache.GetStatus(id) if ok { @@ -1472,11 +1465,7 @@ func (c *Client) UpdateChatNicknames() { } } - gateway.SendPresence( - c.xmpp, - c.jid, - newArgs..., - ) + c.sendPresence(newArgs...) gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index dfe2ebf..981858d 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -3,6 +3,7 @@ package gateway import ( "encoding/xml" "github.com/pkg/errors" + "strconv" "strings" "sync" @@ -343,6 +344,20 @@ func SendPresence(component *xmpp.Component, to string, args ...args.V) error { return nil } +// SPAppendFrom appends numeric from and resource to varargs +func SPAppendFrom(oldArgs []args.V, id int64) []args.V { + newArgs := append(oldArgs, SPFrom(strconv.FormatInt(id, 10))) + newArgs = append(newArgs, SPResource(Jid.Resource)) + return newArgs +} + +// SimplePresence crafts simple presence varargs +func SimplePresence(from int64, typ string) []args.V { + args := []args.V{SPType(typ)} + args = SPAppendFrom(args, from) + return args +} + // ResumableSend tries to resume the connection once and sends the packet again func ResumableSend(component *xmpp.Component, packet stanza.Packet) error { err := component.Send(packet) From b9b6ba14a442f3c4394c535461bd6b1d03a7ef7b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 24 Jan 2024 18:54:25 -0500 Subject: [PATCH 09/54] Fix stuck logout --- go.mod | 2 +- go.sum | 2 ++ telegram/connect.go | 4 ++-- telegram/utils.go | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 50e753d..949667e 100644 --- a/go.mod +++ b/go.mod @@ -34,4 +34,4 @@ require ( ) replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f -replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268 +replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 diff --git a/go.sum b/go.sum index 6fd2f3e..d44752b 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615 h1:RRUZJSro dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268 h1:NCbc2bYuUGQsb/3z5SCIia3N34Ktwq3FwaUAfgF/WEU= dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= +dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 h1:CWAQT74LwQne/3Po5KXDvudu3N0FBWm3XZZZhtl5j2w= +dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/telegram/connect.go b/telegram/connect.go index 6c88cd1..afa2d1f 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -68,10 +68,10 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth return nil case client.TypeAuthorizationStateLoggingOut: - return client.ErrNotSupportedAuthorizationState + return nil case client.TypeAuthorizationStateClosing: - return client.ErrNotSupportedAuthorizationState + return nil case client.TypeAuthorizationStateClosed: return client.ErrNotSupportedAuthorizationState diff --git a/telegram/utils.go b/telegram/utils.go index f0316f2..3f15488 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -362,10 +362,10 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten if message.ReplyTo != nil && message.ReplyTo.MessageReplyToType() == client.TypeMessageReplyToMessage { replyTo, _ := message.ReplyTo.(*client.MessageReplyToMessage) var text string - if replyTo.Quote != nil && !noContent { + if replyTo.Quote != nil && replyTo.Quote.Text != nil && !noContent { text = formatter.Format( - replyTo.Quote.Text, - replyTo.Quote.Entities, + replyTo.Quote.Text.Text, + replyTo.Quote.Text.Entities, c.getFormatter(), ) // make the whole quote fit one line From e37c428c6764eff8e7b5ea286b1c7a0ba52be11a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 26 Jan 2024 21:02:47 -0500 Subject: [PATCH 10/54] XEP-0333 read markers for outgoing messages --- telegram/client.go | 8 ++++++-- telegram/handlers.go | 45 +++++++++++++++++++++++++++++++++++++++-- telegram/utils.go | 27 +++++++++++++++++++++---- xmpp/gateway/gateway.go | 36 ++++++++++++++++++++++++++++----- xmpp/handlers.go | 7 +++++-- 5 files changed, 108 insertions(+), 15 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 49fc1ef..38dff4c 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -34,11 +34,13 @@ type Client struct { jid string Session *persistence.Session resources map[string]bool - outbox map[string]string content *config.TelegramContentConfig cache *cache.Cache online bool + outbox map[string]string + editOutbox map[string]string + DelayedStatuses map[int64]*DelayedStatus DelayedStatusesLock sync.Mutex @@ -54,6 +56,7 @@ type clientLocks struct { chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex outboxLock sync.Mutex + editOutboxLock sync.Mutex lastMsgHashesLock sync.Mutex authorizerReadLock sync.Mutex @@ -109,9 +112,10 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component jid: jid, Session: session, resources: make(map[string]bool), - outbox: make(map[string]string), content: &conf.Content, cache: cache.NewCache(), + outbox: make(map[string]string), + editOutbox: make(map[string]string), options: options, DelayedStatuses: make(map[int64]*DelayedStatus), lastMsgHashes: make(map[int64]uint64), diff --git a/telegram/handlers.go b/telegram/handlers.go index 0d1cda9..6f6d339 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -55,6 +55,33 @@ func (c *Client) cleanTempFile(path string) { } } +func (c *Client) sendMarker(chatId, messageId int64, typ gateway.MarkerType) { + if xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, messageId); err == nil { + resource := c.getFromOutbox(xmppId) + + var stringType string + if typ == gateway.MarkerTypeReceived { + stringType = "received" + } else if typ == gateway.MarkerTypeDisplayed { + stringType = "displayed" + } + log.WithFields(log.Fields{ + "xmppId": xmppId, + "resource": resource, + }).Debugf("marker: %s", stringType) + + if resource != "" { + gateway.SendMessageMarker( + c.jid+"/"+resource, + strconv.FormatInt(chatId, 10), + c.xmpp, + typ, + xmppId, + ) + } + } +} + func (c *Client) updateHandler() { listener := c.client.GetListener() defer listener.Close() @@ -141,6 +168,12 @@ func (c *Client) updateHandler() { uhOh() } c.updateChatTitle(typedUpdate) + case client.TypeUpdateChatReadOutbox: + typedUpdate, ok := update.(*client.UpdateChatReadOutbox) + if !ok { + uhOh() + } + c.updateChatReadOutbox(typedUpdate) default: // log only handled types continue @@ -239,7 +272,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId) var ignoredResource string if err == nil { - ignoredResource = c.popFromOutbox(xmppId) + ignoredResource = c.popFromEditOutbox(xmppId) } else { log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId) } @@ -294,19 +327,23 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat } } -// clean uploaded files func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { + // replace message ID in local database log.Debugf("replace message %v with %v", update.OldMessageId, update.Message.Id) if err := gateway.IdsDB.ReplaceTgId(c.Session.Login, c.jid, update.Message.ChatId, update.OldMessageId, update.Message.Id); err != nil { log.Errorf("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) } + c.sendMarker(update.Message.ChatId, update.Message.Id, gateway.MarkerTypeReceived) + + // clean uploaded files file, _ := c.contentToFile(update.Message.Content) if file != nil && file.Local != nil { c.cleanTempFile(file.Local.Path) } } func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) { + // clean uploaded files file, _ := c.contentToFile(update.Message.Content) if file != nil && file.Local != nil { c.cleanTempFile(file.Local.Path) @@ -328,3 +365,7 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { chat.Title = update.Title } } + +func (c *Client) updateChatReadOutbox(update *client.UpdateChatReadOutbox) { + c.sendMarker(update.ChatId, update.LastReadOutboxMessageId, gateway.MarkerTypeDisplayed) +} diff --git a/telegram/utils.go b/telegram/utils.go index 3f15488..6aab03a 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1472,6 +1472,27 @@ func (c *Client) UpdateChatNicknames() { } } +// AddToEditOutbox temporarily store the resource from which a replace message with given ID was sent +func (c *Client) AddToEditOutbox(xmppId, resource string) { + c.locks.editOutboxLock.Lock() + defer c.locks.editOutboxLock.Unlock() + + c.editOutbox[xmppId] = resource +} + +func (c *Client) popFromEditOutbox(xmppId string) string { + c.locks.editOutboxLock.Lock() + defer c.locks.editOutboxLock.Unlock() + + resource, ok := c.editOutbox[xmppId] + if ok { + delete(c.editOutbox, xmppId) + } else { + log.Warnf("No %v xmppId in edit outbox", xmppId) + } + return resource +} + // AddToOutbox remembers the resource from which a message with given ID was sent func (c *Client) AddToOutbox(xmppId, resource string) { c.locks.outboxLock.Lock() @@ -1480,14 +1501,12 @@ func (c *Client) AddToOutbox(xmppId, resource string) { c.outbox[xmppId] = resource } -func (c *Client) popFromOutbox(xmppId string) string { +func (c *Client) getFromOutbox(xmppId string) string { c.locks.outboxLock.Lock() defer c.locks.outboxLock.Unlock() resource, ok := c.outbox[xmppId] - if ok { - delete(c.outbox, xmppId) - } else { + if !ok { log.Warnf("No %v xmppId in outbox", xmppId) } return resource diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 981858d..4b2a07f 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -23,6 +23,17 @@ type Reply struct { End uint64 } +type MarkerType byte +const ( + MarkerTypeReceived MarkerType = iota + MarkerTypeDisplayed +) + +type marker struct { + Type MarkerType + Id string +} + const NSNick string = "http://jabber.org/protocol/nick" // Queue stores presences to send later @@ -44,25 +55,33 @@ var MessageOutgoingPermissionVersion = 0 // SendMessage creates and sends a message stanza func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon bool) { - sendMessageWrapper(to, from, body, id, component, reply, "", isCarbon) + sendMessageWrapper(to, from, body, id, component, reply, nil, "", isCarbon) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to string, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", component, nil, "", false) + sendMessageWrapper(to, "", body, "", component, nil, nil, "", false) } // SendTextMessage creates and sends a simple message stanza func SendTextMessage(to string, from string, body string, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", component, nil, "", false) + sendMessageWrapper(to, from, body, "", component, nil, nil, "", false) } // SendMessageWithOOB creates and sends a message stanza with OOB URL func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { - sendMessageWrapper(to, from, body, id, component, reply, oob, isCarbon) + sendMessageWrapper(to, from, body, id, component, reply, nil, oob, isCarbon) } -func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { +// SendMessageMarker creates and sends a message stanza with a XEP-0333 marker +func SendMessageMarker(to string, from string, component *xmpp.Component, markerType MarkerType, markerId string) { + sendMessageWrapper(to, from, "", "", component, nil, &marker{ + Type: markerType, + Id: markerId, + }, "", false) +} + +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob string, isCarbon bool) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -120,6 +139,13 @@ func sendMessageWrapper(to string, from string, body string, id string, componen message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) } } + if marker != nil { + if marker.Type == MarkerTypeReceived { + message.Extensions = append(message.Extensions, stanza.MarkReceived{ID: marker.Id}) + } else if marker.Type == MarkerTypeDisplayed { + message.Extensions = append(message.Extensions, stanza.MarkDisplayed{ID: marker.Id}) + } + } if !isCarbon && toJid.Resource != "" { message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 4c27b3c..088cb21 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -199,10 +199,12 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { if err != nil { log.Errorf("Failed to replace id %v with %v %v", replace.Id, msg.Id, tgMessageId) } */ - session.AddToOutbox(replace.Id, resource) + session.AddToEditOutbox(replace.Id, resource) } else { err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) - if err != nil { + if err == nil { + session.AddToOutbox(msg.Id, resource) + } else { log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) } } @@ -458,6 +460,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { _, ok := toToID(iq.To) if ok { disco.AddIdentity("", "account", "registered") + disco.AddFeatures(stanza.NSMsgChatMarkers) } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddFeatures("jabber:iq:register") From 81fc3ea3707cdf73b846ab55b2ecc5b8f68ccd21 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 27 Jan 2024 03:25:17 -0500 Subject: [PATCH 11/54] Also ack with XEP-0184 read receipts for outgoing messages --- xmpp/gateway/gateway.go | 1 + xmpp/handlers.go | 1 + 2 files changed, 2 insertions(+) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 4b2a07f..5ba201a 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -144,6 +144,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen message.Extensions = append(message.Extensions, stanza.MarkReceived{ID: marker.Id}) } else if marker.Type == MarkerTypeDisplayed { message.Extensions = append(message.Extensions, stanza.MarkDisplayed{ID: marker.Id}) + message.Extensions = append(message.Extensions, stanza.ReceiptReceived{ID: marker.Id}) } } if !isCarbon && toJid.Resource != "" { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 088cb21..541eb63 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -461,6 +461,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { if ok { disco.AddIdentity("", "account", "registered") disco.AddFeatures(stanza.NSMsgChatMarkers) + disco.AddFeatures(stanza.NSMsgReceipts) } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddFeatures("jabber:iq:register") From 599cf16cdbb8567cf2ab1ce42aee5f493884de96 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 27 Jan 2024 06:13:45 -0500 Subject: [PATCH 12/54] Request and send to Telegram XEP-0333 displayed markers by "receipts" option --- persistence/sessions.go | 11 +++++++++++ telegram/commands.go | 1 + telegram/handlers.go | 2 +- telegram/utils.go | 21 ++++++++++++++------- xmpp/gateway/gateway.go | 19 +++++++++++-------- xmpp/handlers.go | 24 ++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 16 deletions(-) diff --git a/persistence/sessions.go b/persistence/sessions.go index 1658cc9..56ff152 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -42,6 +42,7 @@ type Session struct { OOBMode bool `yaml:":oobmode"` Carbons bool `yaml:":carbons"` HideIds bool `yaml:":hideids"` + Receipts bool `yaml:":receipts"` } var configKeys = []string{ @@ -52,6 +53,7 @@ var configKeys = []string{ "oobmode", "carbons", "hideids", + "receipts", } var sessionDB *SessionsYamlDB @@ -130,6 +132,8 @@ func (s *Session) Get(key string) (string, error) { return fromBool(s.Carbons), nil case "hideids": return fromBool(s.HideIds), nil + case "receipts": + return fromBool(s.Receipts), nil } return "", errors.New("Unknown session property") @@ -194,6 +198,13 @@ func (s *Session) Set(key string, value string) (string, error) { } s.HideIds = b return value, nil + case "receipts": + b, err := toBool(value) + if err != nil { + return "", err + } + s.Receipts = b + return value, nil } return "", errors.New("Unknown session property") diff --git a/telegram/commands.go b/telegram/commands.go index 19fd655..b5c856e 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -202,6 +202,7 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { c.xmpp, reply, false, + false, ) } } diff --git a/telegram/handlers.go b/telegram/handlers.go index 6f6d339..c715932 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -298,7 +298,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { markupFunction, )) for _, jid := range jids { - gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false) + gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false, false) } } } diff --git a/telegram/utils.go b/telegram/utils.go index 6aab03a..966c5c2 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1074,24 +1074,31 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } // mark message as read - c.client.ViewMessages(&client.ViewMessagesRequest{ - ChatId: chatId, - MessageIds: []int64{message.Id}, - ForceRead: true, - }) + if !c.Session.Receipts { + c.MarkAsRead(chatId, message.Id) + } // forward message to XMPP sId := strconv.FormatInt(message.Id, 10) sChatId := strconv.FormatInt(chatId, 10) for _, jid := range jids { - gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon) + gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon, c.Session.Receipts) if auxText != "" { - gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon) + gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon, c.Session.Receipts) } } } +// MarkAsRead marks a message as read +func (c *Client) MarkAsRead(chatId, messageId int64) { + c.client.ViewMessages(&client.ViewMessagesRequest{ + ChatId: chatId, + MessageIds: []int64{messageId}, + ForceRead: true, + }) +} + // PrepareMessageContent creates a simple text message func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageContent { return c.prepareOutgoingMessageContent(text, nil) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 5ba201a..7d3cbb6 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -54,23 +54,23 @@ var DirtySessions = false var MessageOutgoingPermissionVersion = 0 // SendMessage creates and sends a message stanza -func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon bool) { - sendMessageWrapper(to, from, body, id, component, reply, nil, "", isCarbon) +func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon, requestReceipt bool) { + sendMessageWrapper(to, from, body, id, component, reply, nil, "", isCarbon, requestReceipt) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to string, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", component, nil, nil, "", false) + sendMessageWrapper(to, "", body, "", component, nil, nil, "", false, false) } // SendTextMessage creates and sends a simple message stanza func SendTextMessage(to string, from string, body string, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", component, nil, nil, "", false) + sendMessageWrapper(to, from, body, "", component, nil, nil, "", false, false) } // SendMessageWithOOB creates and sends a message stanza with OOB URL -func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { - sendMessageWrapper(to, from, body, id, component, reply, nil, oob, isCarbon) +func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon, requestReceipt bool) { + sendMessageWrapper(to, from, body, id, component, reply, nil, oob, isCarbon, requestReceipt) } // SendMessageMarker creates and sends a message stanza with a XEP-0333 marker @@ -78,10 +78,10 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker sendMessageWrapper(to, from, "", "", component, nil, &marker{ Type: markerType, Id: markerId, - }, "", false) + }, "", false, false) } -func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob string, isCarbon bool) { +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob string, isCarbon, requestReceipt bool) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -150,6 +150,9 @@ func sendMessageWrapper(to string, from string, body string, id string, componen if !isCarbon && toJid.Resource != "" { message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } + if requestReceipt { + message.Extensions = append(message.Extensions, stanza.ReceiptRequest{}) + } if isCarbon { carbonMessage := extensions.ClientMessage{ diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 541eb63..9caf886 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -254,6 +254,30 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { gateway.MessageOutgoingPermissionVersion = 2 } } + + var displayed stanza.MarkDisplayed + msg.Get(&displayed) + if displayed.ID != "" { + log.Debugf("displayed: %#v", displayed) + + bare, _, ok := gateway.SplitJID(msg.From) + if !ok { + return + } + session, ok := sessions[bare] + if !ok { + return + } + toID, ok := toToID(msg.To) + if !ok { + return + } + msgId, err := strconv.ParseInt(displayed.ID, 10, 64) + if err == nil { + session.MarkAsRead(toID, msgId) + } + return + } } if msg.Type == "error" { From c141c4ad2bebe51562be0a7cfe0671f34b0a49fb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 27 Jan 2024 06:47:12 -0500 Subject: [PATCH 13/54] Fix markable --- xmpp/gateway/gateway.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 7d3cbb6..b1bcd69 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -151,7 +151,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } if requestReceipt { - message.Extensions = append(message.Extensions, stanza.ReceiptRequest{}) + message.Extensions = append(message.Extensions, stanza.Markable{}) } if isCarbon { From ea004b7f7c11fa0ddf560317fd9d6f9b2869144a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 29 Jan 2024 04:28:15 -0500 Subject: [PATCH 14/54] Reflect Telegram edits natively by nativeedits option --- persistence/sessions.go | 11 +++++++++ telegram/client.go | 3 +++ telegram/commands.go | 14 ++++++++++- telegram/handlers.go | 51 +++++++++++++++++++++++++++++++++-------- telegram/utils.go | 29 +++++++++++++++++++---- xmpp/gateway/gateway.go | 19 ++++++++------- xmpp/handlers.go | 1 + 7 files changed, 104 insertions(+), 24 deletions(-) diff --git a/persistence/sessions.go b/persistence/sessions.go index 56ff152..29c4918 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -43,6 +43,7 @@ type Session struct { Carbons bool `yaml:":carbons"` HideIds bool `yaml:":hideids"` Receipts bool `yaml:":receipts"` + NativeEdits bool `yaml:":nativeedits"` } var configKeys = []string{ @@ -54,6 +55,7 @@ var configKeys = []string{ "carbons", "hideids", "receipts", + "nativeedits", } var sessionDB *SessionsYamlDB @@ -134,6 +136,8 @@ func (s *Session) Get(key string) (string, error) { return fromBool(s.HideIds), nil case "receipts": return fromBool(s.Receipts), nil + case "nativeedits": + return fromBool(s.NativeEdits), nil } return "", errors.New("Unknown session property") @@ -205,6 +209,13 @@ func (s *Session) Set(key string, value string) (string, error) { } s.Receipts = b return value, nil + case "nativeedits": + b, err := toBool(value) + if err != nil { + return "", err + } + s.NativeEdits = b + return value, nil } return "", errors.New("Unknown session property") diff --git a/telegram/client.go b/telegram/client.go index 38dff4c..79f27d5 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -45,6 +45,7 @@ type Client struct { DelayedStatusesLock sync.Mutex lastMsgHashes map[int64]uint64 + lastMsgIds map[int64]string msgHashSeed maphash.Seed locks clientLocks @@ -58,6 +59,7 @@ type clientLocks struct { outboxLock sync.Mutex editOutboxLock sync.Mutex lastMsgHashesLock sync.Mutex + lastMsgIdsLock sync.RWMutex authorizerReadLock sync.Mutex authorizerWriteLock sync.Mutex @@ -119,6 +121,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component options: options, DelayedStatuses: make(map[int64]*DelayedStatus), lastMsgHashes: make(map[int64]uint64), + lastMsgIds: make(map[int64]string), msgHashSeed: maphash.MakeSeed(), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), diff --git a/telegram/commands.go b/telegram/commands.go index b5c856e..9251ebb 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -201,6 +201,7 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { strconv.FormatInt(message.Id, 10), c.xmpp, reply, + "", false, false, ) @@ -380,9 +381,20 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } case "config": if len(args) > 1 { + var msg string if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { return "The server did not allow to enable carbons" } + if !c.Session.RawMessages && args[0] == "nativeedits" && args[1] == "true" { + return "nativeedits only works with rawmessages as of yet, enable it first" + } + if c.Session.NativeEdits && args[0] == "rawmessages" && args[1] == "false" { + _, err := c.Session.Set("nativeedits", "false") + if err != nil { + return err.Error() + } + msg = "Automatically disabling nativeedits too...\n" + } value, err := c.Session.Set(args[0], args[1]) if err != nil { @@ -390,7 +402,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } gateway.DirtySessions = true - return fmt.Sprintf("%s set to %s", args[0], value) + return fmt.Sprintf("%s%s set to %s", msg, args[0], value) } else if len(args) > 0 { value, err := c.Session.Get(args[0]) if err != nil { diff --git a/telegram/handlers.go b/telegram/handlers.go index c715932..dfdd3d5 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -269,9 +269,9 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { c.SendMessageLock.Lock() c.SendMessageLock.Unlock() - xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId) + xmppId, xmppIdErr := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId) var ignoredResource string - if err == nil { + if xmppIdErr == nil { ignoredResource = c.popFromEditOutbox(xmppId) } else { log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId) @@ -286,19 +286,50 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) { textContent := update.NewContent.(*client.MessageText) - var editChar string - if c.Session.AsciiArrows { - editChar = "e " - } else { - editChar = "✎ " + var replaceId string + sId := strconv.FormatInt(update.MessageId, 10) + var isCarbon bool + + // use XEP-0308 edits only if the last message is edited for sure, fallback otherwise + if c.Session.NativeEdits { + lastXmppId, ok := c.getLastChatMessageId(update.ChatId) + if xmppIdErr != nil { + xmppId = sId + } + if ok && lastXmppId == xmppId { + replaceId = xmppId + message, err := c.client.GetMessage(&client.GetMessageRequest{ + ChatId: update.ChatId, + MessageId: update.MessageId, + }) + if err == nil { + isCarbon = c.isCarbonsEnabled() && message.IsOutgoing + } else { + log.Errorf("No message %v/%v found, cannot reliably determine if it's a carbon", update.ChatId, update.MessageId) + } + } else { + log.Infof("Mismatching message ids: %v %v, falling back to separate edit message", lastXmppId, xmppId) + } } - text := editChar + fmt.Sprintf("%v | %s", update.MessageId, formatter.Format( + + text := formatter.Format( textContent.Text.Text, textContent.Text.Entities, markupFunction, - )) + ) + + if replaceId == "" { + var editChar string + if c.Session.AsciiArrows { + editChar = "e " + } else { + editChar = "✎ " + } + text = editChar + fmt.Sprintf("%v | %s", update.MessageId, text) + } + for _, jid := range jids { - gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false, false) + gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+sId, c.xmpp, nil, replaceId, isCarbon, false) } } } diff --git a/telegram/utils.go b/telegram/utils.go index 966c5c2..2082645 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -911,14 +911,17 @@ func (c *Client) countCharsInLines(lines *[]string) (count int) { return } +func (c *Client) isCarbonsEnabled() bool { + return gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons +} + 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) } - isCarbonsEnabled := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons // with carbons, hide for all messages in PM and only for outgoing in group chats - hideSender := isCarbonsEnabled && (message.IsOutgoing || isPM) + hideSender := c.isCarbonsEnabled() && (message.IsOutgoing || isPM) prefix := []string{} // message direction @@ -1007,7 +1010,7 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File { // ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { - isCarbon := gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing + isCarbon := c.isCarbonsEnabled() && message.IsOutgoing jids := c.getCarbonFullJids(isCarbon, "") var text, oob, auxText string @@ -1083,11 +1086,12 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { sChatId := strconv.FormatInt(chatId, 10) for _, jid := range jids { - gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon, c.Session.Receipts) + gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, "", isCarbon, c.Session.Receipts) if auxText != "" { - gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon, c.Session.Receipts) + gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, "", isCarbon, c.Session.Receipts) } } + c.UpdateLastChatMessageId(chatId, sId) } // MarkAsRead marks a message as read @@ -1588,6 +1592,21 @@ func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content clie return !ok || oldHash != newHash } +func (c *Client) UpdateLastChatMessageId(chatId int64, messageId string) { + c.locks.lastMsgIdsLock.Lock() + defer c.locks.lastMsgIdsLock.Unlock() + + c.lastMsgIds[chatId] = messageId +} + +func (c *Client) getLastChatMessageId(chatId int64) (string, bool) { + c.locks.lastMsgIdsLock.RLock() + defer c.locks.lastMsgIdsLock.RUnlock() + + xmppId, ok := c.lastMsgIds[chatId] + return xmppId, ok +} + func (c *Client) getFormatter() formatter.MarkupModeType { return formatter.MarkupModeXEP0393 } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index b1bcd69..736f760 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -54,23 +54,23 @@ var DirtySessions = false var MessageOutgoingPermissionVersion = 0 // SendMessage creates and sends a message stanza -func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon, requestReceipt bool) { - sendMessageWrapper(to, from, body, id, component, reply, nil, "", isCarbon, requestReceipt) +func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, replaceId string, isCarbon, requestReceipt bool) { + sendMessageWrapper(to, from, body, id, component, reply, nil, "", replaceId, isCarbon, requestReceipt) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to string, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", component, nil, nil, "", false, false) + sendMessageWrapper(to, "", body, "", component, nil, nil, "", "", false, false) } // SendTextMessage creates and sends a simple message stanza func SendTextMessage(to string, from string, body string, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", component, nil, nil, "", false, false) + sendMessageWrapper(to, from, body, "", component, nil, nil, "", "", false, false) } // SendMessageWithOOB creates and sends a message stanza with OOB URL -func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon, requestReceipt bool) { - sendMessageWrapper(to, from, body, id, component, reply, nil, oob, isCarbon, requestReceipt) +func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob, replaceId string, isCarbon, requestReceipt bool) { + sendMessageWrapper(to, from, body, id, component, reply, nil, oob, replaceId, isCarbon, requestReceipt) } // SendMessageMarker creates and sends a message stanza with a XEP-0333 marker @@ -78,10 +78,10 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker sendMessageWrapper(to, from, "", "", component, nil, &marker{ Type: markerType, Id: markerId, - }, "", false, false) + }, "", "", false, false) } -func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob string, isCarbon, requestReceipt bool) { +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob, replaceId string, isCarbon, requestReceipt bool) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -153,6 +153,9 @@ func sendMessageWrapper(to string, from string, body string, id string, componen if requestReceipt { message.Extensions = append(message.Extensions, stanza.Markable{}) } + if replaceId != "" { + message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId}) + } if isCarbon { carbonMessage := extensions.ClientMessage{ diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 9caf886..8c6ba37 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -204,6 +204,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) if err == nil { session.AddToOutbox(msg.Id, resource) + session.UpdateLastChatMessageId(toID, msg.Id) } else { log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) } From 3a60a1cfaa9329e4c89b8c160892dda75de6c560 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 29 Jan 2024 04:50:57 -0500 Subject: [PATCH 15/54] Bump Makefile to TDLib commit with the logout fix --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4d1a263..cf7b441 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: all test COMMIT := $(shell git rev-parse --short HEAD) -TD_COMMIT := "3870c29b158b75ca5e48e0eebd6b5c3a7994a000" +TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" VERSION := "v1.9.0-dev" MAKEOPTS := "-j4" From 20e6d2558e868d61d7168ffe5ca4f3bff0a240c7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 29 Jan 2024 05:00:42 -0500 Subject: [PATCH 16/54] Version 1.9.0 --- Makefile | 2 +- persistence/sessions_test.go | 3 +++ telegabber.go | 2 +- telegram/connect.go | 2 +- telegram/utils.go | 2 +- telegram/utils_test.go | 8 +++++--- xmpp/gateway/gateway.go | 1 + 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index cf7b441..cadda1c 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.0-dev" +VERSION := "v1.9.0" MAKEOPTS := "-j4" all: diff --git a/persistence/sessions_test.go b/persistence/sessions_test.go index 8ca6f4f..0339378 100644 --- a/persistence/sessions_test.go +++ b/persistence/sessions_test.go @@ -48,6 +48,7 @@ func TestSessionToMap(t *testing.T) { Timezone: "klsf", RawMessages: true, OOBMode: true, + Receipts: true, } m := session.ToMap() sample := map[string]string{ @@ -58,6 +59,8 @@ func TestSessionToMap(t *testing.T) { "oobmode": "true", "carbons": "false", "hideids": "false", + "receipts": "true", + "nativeedits": "false", } if !reflect.DeepEqual(m, sample) { t.Errorf("Map does not match the sample: %v", m) diff --git a/telegabber.go b/telegabber.go index 8db6077..8edd416 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.0-dev" +var version string = "1.9.0" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/connect.go b/telegram/connect.go index afa2d1f..f344759 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -158,7 +158,7 @@ func (c *Client) Connect(resource string) error { } gateway.SubscribeToTransport(c.xmpp, c.jid) - c.sendPresence(gateway.SPStatus("Logged in as: "+c.Session.Login)) + c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login)) }() return nil diff --git a/telegram/utils.go b/telegram/utils.go index 2082645..7ab5765 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1298,7 +1298,7 @@ func (c *Client) roster(resource string) { c.ProcessStatusUpdate(chat, "", "") } - c.sendPresence(gateway.SPStatus("Logged in as: "+c.Session.Login)) + c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login)) c.addResource(resource) } diff --git a/telegram/utils_test.go b/telegram/utils_test.go index 534596f..fa9c107 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -519,9 +519,11 @@ func TestMessageToPrefix6(t *testing.T) { ChatId: 25, IsOutgoing: true, ReplyTo: &client.MessageReplyToMessage{ - ChatId: 41, - Quote: &client.FormattedText{ - Text: "tist\nuz\niz", + ChatId: 41, + Quote: &client.TextQuote{ + Text: &client.FormattedText{ + Text: "tist\nuz\niz", + }, }, Origin: &client.MessageOriginHiddenUser{ SenderName: "ziz", diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 736f760..de0ec8d 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -24,6 +24,7 @@ type Reply struct { } type MarkerType byte + const ( MarkerTypeReceived MarkerType = iota MarkerTypeDisplayed From fd0d7411c2f5e1e0368d3494318ce05195d0a56d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 30 Jan 2024 21:38:46 -0500 Subject: [PATCH 17/54] Basic Ad-Hoc support for transport commands --- go.mod | 2 +- go.sum | 2 + telegram/commands.go | 58 +++++++++++++++++-------- xmpp/handlers.go | 100 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 133 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 949667e..fe7aeb4 100644 --- a/go.mod +++ b/go.mod @@ -33,5 +33,5 @@ require ( nhooyr.io/websocket v1.6.5 // indirect ) -replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f +replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 diff --git a/go.sum b/go.sum index d44752b..f5e218f 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 h1:CWAQT74L dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4= dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd h1:+UW+E7JjI88aH4beDn1cw6D8rs1I061hN91HU4Y4pT8= +dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/telegram/commands.go b/telegram/commands.go index 9251ebb..1c10d12 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -48,6 +48,7 @@ var permissionsMember = client.ChatPermissions{ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ + "help": command{"", "help"}, "login": command{"phone", "sign in"}, "logout": command{"", "sign out"}, "cancelauth": command{"", "quit the signin wizard"}, @@ -66,6 +67,7 @@ var transportCommands = map[string]command{ } var chatCommands = map[string]command{ + "help": command{"", "help"}, "d": command{"[n]", "delete your last message(s)"}, "s": command{"edited message", "edit your last message"}, "silent": command{"message", "send a message without sound"}, @@ -110,38 +112,56 @@ type command struct { } type configurationOption command -type helpType int +// CommandType disinguishes command sets by chat +type CommandType int const ( - helpTypeTransport helpType = iota - helpTypeChat + CommandTypeTransport CommandType = iota + CommandTypeChat ) -func helpString(ht helpType) string { - var str strings.Builder +// GetCommands exposes the set of commands +func GetCommands(typ CommandType) map[string]command { var commandMap map[string]command - switch ht { - case helpTypeTransport: + switch typ { + case CommandTypeTransport: commandMap = transportCommands - case helpTypeChat: + case CommandTypeChat: commandMap = chatCommands } + return commandMap +} + +// CommandToHelpString builds a text description of a command +func CommandToHelpString(name string, cmd command) string { + var str strings.Builder + + str.WriteString("/") + str.WriteString(name) + if cmd.arguments != "" { + str.WriteString(" ") + str.WriteString(cmd.arguments) + } + str.WriteString(" — ") + str.WriteString(cmd.description) + + return str.String() +} + +func helpString(typ CommandType) string { + var str strings.Builder + + commandMap := GetCommands(typ) + str.WriteString("Available commands:\n") for name, command := range commandMap { - str.WriteString("/") - str.WriteString(name) - if command.arguments != "" { - str.WriteString(" ") - str.WriteString(command.arguments) - } - str.WriteString(" — ") - str.WriteString(command.description) + str.WriteString(CommandToHelpString(name, command)) str.WriteString("\n") } - if ht == helpTypeTransport { + if typ == CommandTypeTransport { str.WriteString("Configuration options\n") for name, option := range transportConfigurationOptions { str.WriteString(name) @@ -448,7 +468,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string case "channel": return c.cmdChannel(args, cmdline) case "help": - return helpString(helpTypeTransport) + return helpString(CommandTypeTransport) } return "" @@ -1088,7 +1108,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return strings.Join(entries, "\n"), true case "help": - return helpString(helpTypeChat), true + return helpString(CommandTypeChat), true default: return "", false } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 8c6ba37..a062f0c 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -26,6 +26,7 @@ const ( TypeVCard4 ) const NodeVCard4 string = "urn:xmpp:vcard4" +const NSCommand string = "http://jabber.org/protocol/commands" func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) @@ -53,14 +54,14 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { return } } - _, ok = iq.Payload.(*stanza.DiscoInfo) + discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { - go handleGetDiscoInfo(s, iq) + go handleGetDiscoInfo(s, iq, discoInfo) return } - _, ok = iq.Payload.(*stanza.DiscoItems) + discoItems, ok := iq.Payload.(*stanza.DiscoItems) if ok { - go handleGetDiscoItems(s, iq) + go handleGetDiscoItems(s, iq, discoItems) return } _, ok = iq.Payload.(*extensions.QueryRegister) @@ -74,6 +75,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleSetQueryRegister(s, iq, query) return } + command, ok := iq.Payload.(*stanza.Command) + if ok { + go handleSetQueryCommand(s, iq, command) + return + } } } @@ -468,7 +474,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { _ = gateway.ResumableSend(component, &answer) } -func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { +func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, @@ -488,8 +494,20 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { disco.AddFeatures(stanza.NSMsgChatMarkers) disco.AddFeatures(stanza.NSMsgReceipts) } else { - disco.AddIdentity("Telegram Gateway", "gateway", "telegram") - disco.AddFeatures("jabber:iq:register") + if di.Node == "" { + disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + disco.AddFeatures("jabber:iq:register") + disco.AddFeatures(NSCommand) + } else { + for name, command := range telegram.GetCommands(telegram.CommandTypeTransport) { + if di.Node == name { + answer.Payload = di + di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node") + di.AddFeatures(NSCommand, "jabber:x:data") + break + } + } + } } answer.Payload = disco @@ -504,7 +522,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { _ = gateway.ResumableSend(component, answer) } -func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { +func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, @@ -517,7 +535,20 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { return } - answer.Payload = answer.DiscoItems() + log.Debugf("discoItems: %#v", di) + + _, ok := toToID(iq.To) + if !ok { + commands := telegram.GetCommands(telegram.CommandTypeTransport) + if di.Node == NSCommand { + answer.Payload = di + for name, command := range commands { + di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) + } + } else { + answer.Payload = answer.DiscoItems() + } + } component, ok := s.(*xmpp.Component) if !ok { @@ -647,6 +678,57 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } } +func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command) { + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + answer, err := stanza.NewIQ(stanza.Attrs{ + Type: stanza.IQTypeResult, + From: iq.To, + To: iq.From, + Id: iq.Id, + Lang: "en", + }) + if err != nil { + log.Errorf("Failed to create answer IQ: %v", err) + return + } + + defer gateway.ResumableSend(component, answer) + + log.Debugf("command: %#v", command) + + if command.Action == "" || command.Action == stanza.CommandActionExecute { + _, ok := toToID(iq.To) + if !ok { + bare, resource, ok := gateway.SplitJID(iq.From) + if !ok { + return + } + + session, ok := sessions[bare] + if !ok { + return + } + + response := session.ProcessTransportCommand("/" + command.Node, resource) + + answer.Payload = &stanza.Command{ + Node: command.Node, + Status: stanza.CommandStatusCompleted, + CommandElement: &stanza.Note{ + Text: response, + Type: stanza.CommandNoteTypeInfo, + }, + } + log.Debugf("command response: %#v", answer.Payload) + } + } +} + func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { answer.Type = stanza.IQTypeError answer.Payload = *payload From f56e6ac1870a946e2084edef0b496d645a826706 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 31 Jan 2024 09:23:07 -0500 Subject: [PATCH 18/54] Eliminate edit echos for outgoing messages --- Makefile | 2 +- telegabber.go | 2 +- telegram/handlers.go | 12 ++++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index cadda1c..c859606 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.0" +VERSION := "v1.9.1" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 8edd416..f42e266 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.0" +var version string = "1.9.1" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/handlers.go b/telegram/handlers.go index dfdd3d5..425309e 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -244,6 +244,8 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { lock.Lock() defer lock.Unlock() + c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) + // ignore self outgoing messages if update.Message.IsOutgoing && update.Message.SendingState != nil && @@ -256,8 +258,6 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { }).Warn("New message from chat") c.ProcessIncomingMessage(chatId, update.Message) - - c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) }() } @@ -267,8 +267,14 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent) + log.Debugf("newContent: %#v", update.NewContent) + + lock := c.getChatMessageLock(update.ChatId) + lock.Lock() + lock.Unlock() c.SendMessageLock.Lock() c.SendMessageLock.Unlock() + xmppId, xmppIdErr := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId) var ignoredResource string if xmppIdErr == nil { @@ -286,6 +292,8 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) { textContent := update.NewContent.(*client.MessageText) + log.Debugf("textContent: %#v", textContent.Text) + var replaceId string sId := strconv.FormatInt(update.MessageId, 10) var isCarbon bool From e3a51919051a5597b49e7b8825f22ddfb24ddc58 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 1 Feb 2024 12:14:06 -0500 Subject: [PATCH 19/54] Declaratively specify optional and required command arguments --- telegram/commands.go | 133 +++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 1c10d12..47438e0 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -48,56 +48,56 @@ var permissionsMember = client.ChatPermissions{ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ - "help": command{"", "help"}, - "login": command{"phone", "sign in"}, - "logout": command{"", "sign out"}, - "cancelauth": command{"", "quit the signin wizard"}, - "code": command{"", "check one-time code"}, - "password": command{"", "check 2fa password"}, - "setusername": command{"", "update @username"}, - "setname": command{"first last", "update name"}, - "setbio": command{"", "update about"}, - "setpassword": command{"[old] [new]", "set or remove password"}, - "config": command{"[param] [value]", "view or update configuration options"}, - "report": command{"[chat] [comment]", "report a chat by id or @username"}, - "add": command{"@username", "add @username to your chat list"}, - "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, - "supergroup": command{"title description", "create new supergroup «title» with «description»"}, - "channel": command{"title description", "create new channel «title» with «description»"}, + "help": command{0, []string{}, "help"}, + "login": command{1, []string{"phone"}, "sign in"}, + "logout": command{0, []string{}, "sign out"}, + "cancelauth": command{0, []string{}, "quit the signin wizard"}, + "code": command{1, []string{"xxxxx"}, "check one-time code"}, + "password": command{1, []string{"********"}, "check 2fa password"}, + "setusername": command{0, []string{"@username"}, "update @username"}, + "setname": command{1, []string{"first", "last"}, "update name"}, + "setbio": command{0, []string{"Lorem ipsum"}, "update about"}, + "setpassword": command{0, []string{"old", "new"}, "set or remove password"}, + "config": command{0, []string{"param", "value"}, "view or update configuration options"}, + "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username"}, + "add": command{1, []string{"@username"}, "add @username to your chat list"}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname"}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»"}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»"}, } var chatCommands = map[string]command{ - "help": command{"", "help"}, - "d": command{"[n]", "delete your last message(s)"}, - "s": command{"edited message", "edit your last message"}, - "silent": command{"message", "send a message without sound"}, - "schedule": command{"{online | 2006-01-02T15:04:05 | 15:04:05} message", "schedules a message either to timestamp or to whenever the user goes online"}, - "forward": command{"message_id target_chat", "forwards a message"}, - "vcard": command{"", "print vCard as text"}, - "add": command{"@username", "add @username to your chat list"}, - "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, - "group": command{"title", "create groupchat «title» with current user"}, - "supergroup": command{"title description", "create new supergroup «title» with «description»"}, - "channel": command{"title description", "create new channel «title» with «description»"}, - "secret": command{"", "create secretchat with current user"}, - "search": command{"string [limit]", "search in current chat"}, - "history": command{"[limit]", "get last [limit] messages from current chat"}, - "block": command{"", "blacklist current user"}, - "unblock": command{"", "unblacklist current user"}, - "invite": command{"id or @username", "add user to current chat"}, - "link": command{"", "get invite link for current chat"}, - "kick": command{"id or @username", "remove user to current chat"}, - "mute": command{"id or @username [hours]", "mute user in current chat"}, - "unmute": command{"id or @username", "unrestrict user from current chat"}, - "ban": command{"id or @username [hours]", "restrict @username from current chat for [hours] or forever"}, - "unban": command{"id or @username", "unbans @username in current chat (and devotes from admins)"}, - "promote": command{"id or @username [title]", "promote user to admin in current chat"}, - "leave": command{"", "leave current chat"}, - "leave!": command{"", "leave current chat (for owners)"}, - "ttl": command{"", "set secret chat messages TTL before self-destroying (in seconds)"}, - "close": command{"", "close current secret chat"}, - "delete": command{"", "delete current chat from chat list"}, - "members": command{"[query]", "search members [by optional query] in current chat (requires admin rights)"}, + "help": command{0, []string{}, "help"}, + "d": command{0, []string{"n"}, "delete your last message(s)"}, + "s": command{1, []string{"edited message"}, "edit your last message"}, + "silent": command{1, []string{"message"}, "send a message without sound"}, + "schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online"}, + "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message"}, + "vcard": command{0, []string{}, "print vCard as text"}, + "add": command{1, []string{"@username"}, "add @username to your chat list"}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname"}, + "group": command{1, []string{"title"}, "create groupchat «title» with current user"}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»"}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»"}, + "secret": command{0, []string{}, "create secretchat with current user"}, + "search": command{0, []string{"string", "[limit]"}, "search in current chat"}, + "history": command{0, []string{"limit"}, "get last [limit] messages from current chat"}, + "block": command{0, []string{}, "blacklist current user"}, + "unblock": command{0, []string{}, "unblacklist current user"}, + "invite": command{1, []string{"id or @username"}, "add user to current chat"}, + "link": command{0, []string{}, "get invite link for current chat"}, + "kick": command{1, []string{"id or @username"}, "remove user to current chat"}, + "mute": command{1, []string{"id or @username", "hours"}, "mute user in current chat"}, + "unmute": command{1, []string{"id or @username"}, "unrestrict user from current chat"}, + "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever"}, + "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)"}, + "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat"}, + "leave": command{0, []string{}, "leave current chat"}, + "leave!": command{0, []string{}, "leave current chat (for owners)"}, + "ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying"}, + "close": command{0, []string{}, "close current secret chat"}, + "delete": command{0, []string{}, "delete current chat from chat list"}, + "members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)"}, } var transportConfigurationOptions = map[string]configurationOption{ @@ -107,10 +107,14 @@ var transportConfigurationOptions = map[string]configurationOption{ } type command struct { + requiredArgs int + arguments []string + description string +} +type configurationOption struct { arguments string description string } -type configurationOption command // CommandType disinguishes command sets by chat type CommandType int @@ -140,9 +144,16 @@ func CommandToHelpString(name string, cmd command) string { str.WriteString("/") str.WriteString(name) - if cmd.arguments != "" { + for i, arg := range cmd.arguments { + optional := i >= cmd.requiredArgs str.WriteString(" ") - str.WriteString(cmd.arguments) + if optional { + str.WriteString("[") + } + str.WriteString(arg) + if optional { + str.WriteString("]") + } } str.WriteString(" — ") str.WriteString(cmd.description) @@ -252,16 +263,20 @@ func (c *Client) usernameOrIDToID(username string) (int64, error) { // and returns a response func (c *Client) ProcessTransportCommand(cmdline string, resource string) string { cmd, args := parseCommand(cmdline) + command, ok := transportCommands[cmd] + if !ok { + return "Unknown command" + } + if len(args) < command.requiredArgs { + return notEnoughArguments + } + switch cmd { case "login", "code", "password": if cmd == "login" && c.Session.Login != "" { return "Phone number already provided, use /cancelauth to start over" } - if len(args) < 1 { - return notEnoughArguments - } - if cmd == "login" { err := c.TryLogin(resource, args[0]) if err != nil { @@ -336,11 +351,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } // set My Name case "setname": - var firstname string + firstname := args[0] var lastname string - if len(args) > 0 { - firstname = args[0] - } + if firstname == "" { return "The name should contain at least one character" } @@ -439,10 +452,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string return strings.Join(entries, "\n") case "report": - if len(args) < 2 { - return "Not enough arguments" - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error() From 21dc5fa6c6c843fcf263b6483d21bc4b284aad1c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 3 Feb 2024 04:24:22 -0500 Subject: [PATCH 20/54] Form support for transport Ad-Hoc commands with arguments --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 21 ++++++---- xmpp/handlers.go | 99 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 95 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index c859606..309a27e 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.1" +VERSION := "v1.10.0-dev" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index f42e266..d39820d 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.1" +var version string = "1.10.0-dev" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 47438e0..4dafdf5 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -107,9 +107,9 @@ var transportConfigurationOptions = map[string]configurationOption{ } type command struct { - requiredArgs int - arguments []string - description string + RequiredArgs int + Arguments []string + Description string } type configurationOption struct { arguments string @@ -138,14 +138,21 @@ func GetCommands(typ CommandType) map[string]command { return commandMap } +// GetCommand obtains one command +func GetCommand(typ CommandType, cmd string) (command, bool) { + commands := GetCommands(typ) + command, ok := commands[cmd] + return command, ok +} + // CommandToHelpString builds a text description of a command func CommandToHelpString(name string, cmd command) string { var str strings.Builder str.WriteString("/") str.WriteString(name) - for i, arg := range cmd.arguments { - optional := i >= cmd.requiredArgs + for i, arg := range cmd.Arguments { + optional := i >= cmd.RequiredArgs str.WriteString(" ") if optional { str.WriteString("[") @@ -156,7 +163,7 @@ func CommandToHelpString(name string, cmd command) string { } } str.WriteString(" — ") - str.WriteString(cmd.description) + str.WriteString(cmd.Description) return str.String() } @@ -267,7 +274,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string if !ok { return "Unknown command" } - if len(args) < command.requiredArgs { + if len(args) < command.RequiredArgs { return notEnoughArguments } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a062f0c..c71fc19 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/pkg/errors" "io" + "sort" "strconv" "strings" @@ -701,31 +702,89 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command log.Debugf("command: %#v", command) + bare, resource, ok := gateway.SplitJID(iq.From) + if !ok { + return + } + + var cmdString string if command.Action == "" || command.Action == stanza.CommandActionExecute { _, ok := toToID(iq.To) if !ok { - bare, resource, ok := gateway.SplitJID(iq.From) - if !ok { - return + cmd, ok := telegram.GetCommand(telegram.CommandTypeTransport, command.Node) + if ok && cmd.RequiredArgs > 0 { + var fields []*stanza.Field + for i, arg := range cmd.Arguments { + fields = append(fields, &stanza.Field{ + Var: strconv.FormatInt(int64(i), 10), + Label: arg, + }) + } + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusExecuting, + CommandElement: &stanza.Form{ + Title: command.Node, + Instructions: []string{cmd.Description}, + Fields: fields, + }, + } + } else { + cmdString = "/" + command.Node } - - session, ok := sessions[bare] - if !ok { - return - } - - response := session.ProcessTransportCommand("/" + command.Node, resource) - - answer.Payload = &stanza.Command{ - Node: command.Node, - Status: stanza.CommandStatusCompleted, - CommandElement: &stanza.Note{ - Text: response, - Type: stanza.CommandNoteTypeInfo, - }, - } - log.Debugf("command response: %#v", answer.Payload) } + } else if command.Action == stanza.CommandActionComplete { + _, ok := toToID(iq.To) + if !ok { + form, ok := command.CommandElement.(*stanza.Form) + if ok { + // just for the case the client messed the order somehow + sort.Slice(form.Fields, func(i int, j int) bool { + iField := form.Fields[i] + jField := form.Fields[j] + if iField != nil && jField != nil { + ii, iErr := strconv.ParseInt(iField.Var, 10, 64) + ji, jErr := strconv.ParseInt(jField.Var, 10, 64) + return iErr == nil && jErr == nil && ii < ji + } + return false + }) + + var cmd strings.Builder + cmd.WriteString("/") + cmd.WriteString(command.Node) + for _, field := range form.Fields { + cmd.WriteString(" ") + if len(field.ValuesList) > 0 { + cmd.WriteString(field.ValuesList[0]) + } + } + + cmdString = cmd.String() + } + } + } + + if cmdString != "" { + session, ok := sessions[bare] + if !ok { + return + } + + response := session.ProcessTransportCommand(cmdString, resource) + + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCompleted, + CommandElement: &stanza.Note{ + Text: response, + Type: stanza.CommandNoteTypeInfo, + }, + } + + log.Debugf("command response: %#v", answer.Payload) } } From e7d5a2a2666adc13c3046e89b30fae87aa5d06e3 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 3 Feb 2024 10:33:37 -0500 Subject: [PATCH 21/54] Accept forms with arbitrary action --- xmpp/handlers.go | 94 ++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c71fc19..c6406e9 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -706,62 +706,60 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if !ok { return } + _, toOk := toToID(iq.To) var cmdString string - if command.Action == "" || command.Action == stanza.CommandActionExecute { - _, ok := toToID(iq.To) - if !ok { - cmd, ok := telegram.GetCommand(telegram.CommandTypeTransport, command.Node) - if ok && cmd.RequiredArgs > 0 { - var fields []*stanza.Field - for i, arg := range cmd.Arguments { - fields = append(fields, &stanza.Field{ - Var: strconv.FormatInt(int64(i), 10), - Label: arg, - }) + if !toOk { + form, formOk := command.CommandElement.(*stanza.Form) + if formOk { + // just for the case the client messed the order somehow + sort.Slice(form.Fields, func(i int, j int) bool { + iField := form.Fields[i] + jField := form.Fields[j] + if iField != nil && jField != nil { + ii, iErr := strconv.ParseInt(iField.Var, 10, 64) + ji, jErr := strconv.ParseInt(jField.Var, 10, 64) + return iErr == nil && jErr == nil && ii < ji } - answer.Payload = &stanza.Command{ - SessionId: command.Node, - Node: command.Node, - Status: stanza.CommandStatusExecuting, - CommandElement: &stanza.Form{ - Title: command.Node, - Instructions: []string{cmd.Description}, - Fields: fields, - }, + return false + }) + + var cmd strings.Builder + cmd.WriteString("/") + cmd.WriteString(command.Node) + for _, field := range form.Fields { + cmd.WriteString(" ") + if len(field.ValuesList) > 0 { + cmd.WriteString(field.ValuesList[0]) } - } else { - cmdString = "/" + command.Node } - } - } else if command.Action == stanza.CommandActionComplete { - _, ok := toToID(iq.To) - if !ok { - form, ok := command.CommandElement.(*stanza.Form) - if ok { - // just for the case the client messed the order somehow - sort.Slice(form.Fields, func(i int, j int) bool { - iField := form.Fields[i] - jField := form.Fields[j] - if iField != nil && jField != nil { - ii, iErr := strconv.ParseInt(iField.Var, 10, 64) - ji, jErr := strconv.ParseInt(jField.Var, 10, 64) - return iErr == nil && jErr == nil && ii < ji - } - return false - }) - var cmd strings.Builder - cmd.WriteString("/") - cmd.WriteString(command.Node) - for _, field := range form.Fields { - cmd.WriteString(" ") - if len(field.ValuesList) > 0 { - cmd.WriteString(field.ValuesList[0]) + cmdString = cmd.String() + } else { + if command.Action == "" || command.Action == stanza.CommandActionExecute { + cmd, ok := telegram.GetCommand(telegram.CommandTypeTransport, command.Node) + if ok && cmd.RequiredArgs > 0 { + var fields []*stanza.Field + for i, arg := range cmd.Arguments { + fields = append(fields, &stanza.Field{ + Var: strconv.FormatInt(int64(i), 10), + Label: arg, + }) } + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusExecuting, + CommandElement: &stanza.Form{ + Type: stanza.FormTypeForm, + Title: command.Node, + Instructions: []string{cmd.Description}, + Fields: fields, + }, + } + } else { + cmdString = "/" + command.Node } - - cmdString = cmd.String() } } } From a0180eff7551ec89f2a925dba69ba547ad0e5d60 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 3 Feb 2024 10:38:00 -0500 Subject: [PATCH 22/54] Handle command cancelling --- xmpp/handlers.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c6406e9..6fb1af2 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -760,6 +760,12 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command } else { cmdString = "/" + command.Node } + } else if command.Action == stanza.CommandActionCancel { + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCancelled, + } } } } From b0c5302c82b78c2f83ef12545437f31ab5406927 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 10 Feb 2024 13:46:02 -0500 Subject: [PATCH 23/54] Ad-Hoc support for chat commands --- telegram/commands.go | 74 +++--------------- xmpp/handlers.go | 174 ++++++++++++++++++++++++------------------- 2 files changed, 108 insertions(+), 140 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 4dafdf5..d9b9f13 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -14,6 +14,7 @@ import ( "github.com/zelenin/go-tdlib/client" ) +const unknownCommand string = "Unknown command" const notEnoughArguments string = "Not enough arguments" const TelegramNotInitialized string = "Telegram connection is not initialized yet" const TelegramAuthDone string = "Authorization is done already" @@ -272,7 +273,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string cmd, args := parseCommand(cmdline) command, ok := transportCommands[cmd] if !ok { - return "Unknown command" + return unknownCommand } if len(args) < command.RequiredArgs { return notEnoughArguments @@ -498,6 +499,14 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } cmd, args := parseCommand(cmdline) + command, ok := chatCommands[cmd] + if !ok { + return unknownCommand, false + } + if len(args) < command.RequiredArgs { + return notEnoughArguments, true + } + switch cmd { // delete message case "d": @@ -542,9 +551,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if c.me == nil { return "@me is not initialized", true } - if len(args) < 1 { - return "Not enough arguments", true - } messages, err := c.getLastMessages(chatID, "", c.me.Id, 1) if err != nil { @@ -575,10 +581,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // send without sound case "silent": - if len(args) < 1 { - return "Not enough arguments", true - } - content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) if content != nil { @@ -597,10 +599,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // schedule a message to timestamp or to going online case "schedule": - if len(args) < 2 { - return "Not enough arguments", true - } - var state client.MessageSchedulingState var result string due := args[0] @@ -677,10 +675,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // forward a message to chat case "forward": - if len(args) < 2 { - return notEnoughArguments, true - } - messageId, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return "Cannot parse message ID", true @@ -742,10 +736,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // create group chat with current user case "group": - if len(args) < 1 { - return notEnoughArguments, true - } - _, err := c.client.CreateNewBasicGroupChat(&client.CreateNewBasicGroupChatRequest{ UserIds: []int64{chatID}, Title: args[0], @@ -773,10 +763,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // invite @username to current groupchat case "invite": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error(), true @@ -801,10 +787,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return link.InviteLink, true // kick @username from current group chat case "kick": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error(), true @@ -820,10 +802,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // mute @username [n hours] case "mute": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error(), true @@ -851,10 +829,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // unmute @username case "unmute": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error(), true @@ -874,10 +848,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // ban @username from current chat [for N hours] case "ban": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error(), true @@ -903,10 +873,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // unban @username case "unban": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error(), true @@ -922,10 +888,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // promote @username to admin case "promote": - if len(args) < 1 { - return notEnoughArguments, true - } - contact, _, err := c.GetContactByUsername(args[0]) if err != nil { return err.Error(), true @@ -1133,10 +1095,6 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } func (c *Client) cmdAdd(args []string) string { - if len(args) < 1 { - return notEnoughArguments - } - chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: args[0], }) @@ -1153,10 +1111,6 @@ func (c *Client) cmdAdd(args []string) string { } func (c *Client) cmdJoin(args []string) string { - if len(args) < 1 { - return notEnoughArguments - } - if strings.HasPrefix(args[0], "@") { chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: args[0], @@ -1186,10 +1140,6 @@ func (c *Client) cmdJoin(args []string) string { } func (c *Client) cmdSupergroup(args []string, cmdline string) string { - if len(args) < 1 { - return notEnoughArguments - } - _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ Title: args[0], Description: rawCmdArguments(cmdline, 1), @@ -1202,10 +1152,6 @@ func (c *Client) cmdSupergroup(args []string, cmdline string) string { } func (c *Client) cmdChannel(args []string, cmdline string) string { - if len(args) < 1 { - return notEnoughArguments - } - _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ Title: args[0], Description: rawCmdArguments(cmdline, 1), diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 6fb1af2..1885aae 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -490,23 +490,30 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { disco := answer.DiscoInfo() _, ok := toToID(iq.To) - if ok { - disco.AddIdentity("", "account", "registered") - disco.AddFeatures(stanza.NSMsgChatMarkers) - disco.AddFeatures(stanza.NSMsgReceipts) - } else { - if di.Node == "" { + if di.Node == "" { + if ok { + disco.AddIdentity("", "account", "registered") + disco.AddFeatures(stanza.NSMsgChatMarkers) + disco.AddFeatures(stanza.NSMsgReceipts) + } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddFeatures("jabber:iq:register") - disco.AddFeatures(NSCommand) + } + disco.AddFeatures(NSCommand) + } else { + var cmdType telegram.CommandType + if ok { + cmdType = telegram.CommandTypeChat } else { - for name, command := range telegram.GetCommands(telegram.CommandTypeTransport) { - if di.Node == name { - answer.Payload = di - di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node") - di.AddFeatures(NSCommand, "jabber:x:data") - break - } + cmdType = telegram.CommandTypeTransport + } + + for name, command := range telegram.GetCommands(cmdType) { + if di.Node == name { + answer.Payload = di + di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node") + di.AddFeatures(NSCommand, "jabber:x:data") + break } } } @@ -539,16 +546,22 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { log.Debugf("discoItems: %#v", di) _, ok := toToID(iq.To) - if !ok { - commands := telegram.GetCommands(telegram.CommandTypeTransport) - if di.Node == NSCommand { - answer.Payload = di - for name, command := range commands { - di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) - } + if di.Node == NSCommand { + answer.Payload = di + + var cmdType telegram.CommandType + if ok { + cmdType = telegram.CommandTypeChat } else { - answer.Payload = answer.DiscoItems() + cmdType = telegram.CommandTypeTransport } + + commands := telegram.GetCommands(cmdType) + for name, command := range commands { + di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) + } + } else { + answer.Payload = answer.DiscoItems() } component, ok := s.(*xmpp.Component) @@ -706,66 +719,70 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if !ok { return } - _, toOk := toToID(iq.To) + toId, toOk := toToID(iq.To) var cmdString string - if !toOk { - form, formOk := command.CommandElement.(*stanza.Form) - if formOk { - // just for the case the client messed the order somehow - sort.Slice(form.Fields, func(i int, j int) bool { - iField := form.Fields[i] - jField := form.Fields[j] - if iField != nil && jField != nil { - ii, iErr := strconv.ParseInt(iField.Var, 10, 64) - ji, jErr := strconv.ParseInt(jField.Var, 10, 64) - return iErr == nil && jErr == nil && ii < ji - } - return false - }) - - var cmd strings.Builder - cmd.WriteString("/") - cmd.WriteString(command.Node) - for _, field := range form.Fields { - cmd.WriteString(" ") - if len(field.ValuesList) > 0 { - cmd.WriteString(field.ValuesList[0]) - } + var cmdType telegram.CommandType + form, formOk := command.CommandElement.(*stanza.Form) + if toOk { + cmdType = telegram.CommandTypeChat + } else { + cmdType = telegram.CommandTypeTransport + } + if formOk { + // just for the case the client messed the order somehow + sort.Slice(form.Fields, func(i int, j int) bool { + iField := form.Fields[i] + jField := form.Fields[j] + if iField != nil && jField != nil { + ii, iErr := strconv.ParseInt(iField.Var, 10, 64) + ji, jErr := strconv.ParseInt(jField.Var, 10, 64) + return iErr == nil && jErr == nil && ii < ji } + return false + }) - cmdString = cmd.String() - } else { - if command.Action == "" || command.Action == stanza.CommandActionExecute { - cmd, ok := telegram.GetCommand(telegram.CommandTypeTransport, command.Node) - if ok && cmd.RequiredArgs > 0 { - var fields []*stanza.Field - for i, arg := range cmd.Arguments { - fields = append(fields, &stanza.Field{ - Var: strconv.FormatInt(int64(i), 10), - Label: arg, - }) - } - answer.Payload = &stanza.Command{ - SessionId: command.Node, - Node: command.Node, - Status: stanza.CommandStatusExecuting, - CommandElement: &stanza.Form{ - Type: stanza.FormTypeForm, - Title: command.Node, - Instructions: []string{cmd.Description}, - Fields: fields, - }, - } - } else { - cmdString = "/" + command.Node + var cmd strings.Builder + cmd.WriteString("/") + cmd.WriteString(command.Node) + for _, field := range form.Fields { + cmd.WriteString(" ") + if len(field.ValuesList) > 0 { + cmd.WriteString(field.ValuesList[0]) + } + } + + cmdString = cmd.String() + } else { + if command.Action == "" || command.Action == stanza.CommandActionExecute { + cmd, ok := telegram.GetCommand(cmdType, command.Node) + if ok && len(cmd.Arguments) > 0 { + var fields []*stanza.Field + for i, arg := range cmd.Arguments { + fields = append(fields, &stanza.Field{ + Var: strconv.FormatInt(int64(i), 10), + Label: arg, + }) } - } else if command.Action == stanza.CommandActionCancel { answer.Payload = &stanza.Command{ - SessionId: command.Node, - Node: command.Node, - Status: stanza.CommandStatusCancelled, + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusExecuting, + CommandElement: &stanza.Form{ + Type: stanza.FormTypeForm, + Title: command.Node, + Instructions: []string{cmd.Description}, + Fields: fields, + }, } + } else { + cmdString = "/" + command.Node + } + } else if command.Action == stanza.CommandActionCancel { + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCancelled, } } } @@ -776,7 +793,12 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command return } - response := session.ProcessTransportCommand(cmdString, resource) + var response string + if toOk { + response, _ = session.ProcessChatCommand(toId, cmdString) + } else { + response = session.ProcessTransportCommand(cmdString, resource) + } answer.Payload = &stanza.Command{ SessionId: command.Node, From 772246ee4b78883ebacdf594e1fc1d485dcb3a58 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 10 Feb 2024 15:22:24 -0500 Subject: [PATCH 24/54] Mark required fields in forms --- xmpp/handlers.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1885aae..08278d2 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -759,9 +759,15 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if ok && len(cmd.Arguments) > 0 { var fields []*stanza.Field for i, arg := range cmd.Arguments { + var required *string + if i < cmd.RequiredArgs { + dummyString := "" + required = &dummyString + } fields = append(fields, &stanza.Field{ - Var: strconv.FormatInt(int64(i), 10), - Label: arg, + Var: strconv.FormatInt(int64(i), 10), + Label: arg, + Required: required, }) } answer.Payload = &stanza.Command{ From dc6f99dc3ca0906bfd5f9bda9eab618445cfa878 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 10 Feb 2024 16:27:08 -0500 Subject: [PATCH 25/54] Stable command order in help and Ad-Hoc list --- telegram/commands.go | 19 ++++++++++++++++++- xmpp/handlers.go | 3 ++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index d9b9f13..8d4de91 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -3,6 +3,7 @@ package telegram import ( "fmt" "github.com/pkg/errors" + "sort" "strconv" "strings" "time" @@ -146,6 +147,21 @@ func GetCommand(typ CommandType, cmd string) (command, bool) { return command, ok } +// SortedCommandKeys sorts a slice with command keys +func SortedCommandKeys(commandMap map[string]command) []string { + keys := make([]string, len(commandMap)) + + i := 0 + for k := range commandMap { + keys[i] = k + i++ + } + + sort.Strings(keys) + + return keys +} + // CommandToHelpString builds a text description of a command func CommandToHelpString(name string, cmd command) string { var str strings.Builder @@ -175,7 +191,8 @@ func helpString(typ CommandType) string { commandMap := GetCommands(typ) str.WriteString("Available commands:\n") - for name, command := range commandMap { + for _, name := range SortedCommandKeys(commandMap) { + command := commandMap[name] str.WriteString(CommandToHelpString(name, command)) str.WriteString("\n") } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 08278d2..c50dd1c 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -557,7 +557,8 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { } commands := telegram.GetCommands(cmdType) - for name, command := range commands { + for _, name := range telegram.SortedCommandKeys(commands) { + command := commands[name] di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) } } else { From 9b5fee88262f22ad14acf621e992167966af278a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 15 Feb 2024 04:40:57 -0500 Subject: [PATCH 26/54] Filter available commands by chat type --- telegram/commands.go | 127 ++++++++++++++++++++++++++----------------- telegram/utils.go | 46 ++++++++++++++-- xmpp/handlers.go | 25 +++++++++ 3 files changed, 143 insertions(+), 55 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 8d4de91..a01a80e 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -50,56 +50,60 @@ var permissionsMember = client.ChatPermissions{ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ - "help": command{0, []string{}, "help"}, - "login": command{1, []string{"phone"}, "sign in"}, - "logout": command{0, []string{}, "sign out"}, - "cancelauth": command{0, []string{}, "quit the signin wizard"}, - "code": command{1, []string{"xxxxx"}, "check one-time code"}, - "password": command{1, []string{"********"}, "check 2fa password"}, - "setusername": command{0, []string{"@username"}, "update @username"}, - "setname": command{1, []string{"first", "last"}, "update name"}, - "setbio": command{0, []string{"Lorem ipsum"}, "update about"}, - "setpassword": command{0, []string{"old", "new"}, "set or remove password"}, - "config": command{0, []string{"param", "value"}, "view or update configuration options"}, - "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username"}, - "add": command{1, []string{"@username"}, "add @username to your chat list"}, - "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname"}, - "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»"}, - "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»"}, + "help": command{0, []string{}, "help", nil}, + "login": command{1, []string{"phone"}, "sign in", nil}, + "logout": command{0, []string{}, "sign out", nil}, + "cancelauth": command{0, []string{}, "quit the signin wizard", nil}, + "code": command{1, []string{"xxxxx"}, "check one-time code", nil}, + "password": command{1, []string{"********"}, "check 2fa password", nil}, + "setusername": command{0, []string{"@username"}, "update @username", nil}, + "setname": command{1, []string{"first", "last"}, "update name", nil}, + "setbio": command{0, []string{"Lorem ipsum"}, "update about", nil}, + "setpassword": command{0, []string{"old", "new"}, "set or remove password", nil}, + "config": command{0, []string{"param", "value"}, "view or update configuration options", nil}, + "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username", nil}, + "add": command{1, []string{"@username"}, "add @username to your chat list", nil}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", nil}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", nil}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", nil}, } +var notForGroups = []ChatType{ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} +var notForPM = []ChatType{ChatTypePrivate, ChatTypeSecret} +var onlyForSecret = []ChatType{ChatTypePrivate, ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} + var chatCommands = map[string]command{ - "help": command{0, []string{}, "help"}, - "d": command{0, []string{"n"}, "delete your last message(s)"}, - "s": command{1, []string{"edited message"}, "edit your last message"}, - "silent": command{1, []string{"message"}, "send a message without sound"}, - "schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online"}, - "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message"}, - "vcard": command{0, []string{}, "print vCard as text"}, - "add": command{1, []string{"@username"}, "add @username to your chat list"}, - "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname"}, - "group": command{1, []string{"title"}, "create groupchat «title» with current user"}, - "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»"}, - "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»"}, - "secret": command{0, []string{}, "create secretchat with current user"}, - "search": command{0, []string{"string", "[limit]"}, "search in current chat"}, - "history": command{0, []string{"limit"}, "get last [limit] messages from current chat"}, - "block": command{0, []string{}, "blacklist current user"}, - "unblock": command{0, []string{}, "unblacklist current user"}, - "invite": command{1, []string{"id or @username"}, "add user to current chat"}, - "link": command{0, []string{}, "get invite link for current chat"}, - "kick": command{1, []string{"id or @username"}, "remove user to current chat"}, - "mute": command{1, []string{"id or @username", "hours"}, "mute user in current chat"}, - "unmute": command{1, []string{"id or @username"}, "unrestrict user from current chat"}, - "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever"}, - "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)"}, - "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat"}, - "leave": command{0, []string{}, "leave current chat"}, - "leave!": command{0, []string{}, "leave current chat (for owners)"}, - "ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying"}, - "close": command{0, []string{}, "close current secret chat"}, - "delete": command{0, []string{}, "delete current chat from chat list"}, - "members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)"}, + "help": command{0, []string{}, "help", nil}, + "d": command{0, []string{"n"}, "delete your last message(s)", nil}, + "s": command{1, []string{"edited message"}, "edit your last message", nil}, + "silent": command{1, []string{"message"}, "send a message without sound", nil}, + "schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online", nil}, + "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", nil}, + "vcard": command{0, []string{}, "print vCard as text", nil}, + "add": command{1, []string{"@username"}, "add @username to your chat list", nil}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", nil}, + "group": command{1, []string{"title"}, "create groupchat «title» with current user", ¬ForGroups}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", nil}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", nil}, + "secret": command{0, []string{}, "create secretchat with current user", ¬ForGroups}, + "search": command{0, []string{"string", "[limit]"}, "search in current chat", nil}, + "history": command{0, []string{"limit"}, "get last [limit] messages from current chat", nil}, + "block": command{0, []string{}, "blacklist current user", ¬ForGroups}, + "unblock": command{0, []string{}, "unblacklist current user", ¬ForGroups}, + "invite": command{1, []string{"id or @username"}, "add user to current chat", ¬ForPM}, + "link": command{0, []string{}, "get invite link for current chat", ¬ForPM}, + "kick": command{1, []string{"id or @username"}, "remove user from current chat", ¬ForPM}, + "mute": command{1, []string{"id or @username", "hours"}, "mute user in current chat", ¬ForPM}, + "unmute": command{1, []string{"id or @username"}, "unrestrict user from current chat", ¬ForPM}, + "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", ¬ForPM}, + "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", ¬ForPM}, + "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", ¬ForPM}, + "leave": command{0, []string{}, "leave current chat", ¬ForPM}, + "leave!": command{0, []string{}, "leave current chat (for owners)", ¬ForPM}, + "ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying", &onlyForSecret}, + "close": command{0, []string{}, "close current secret chat", &onlyForSecret}, + "delete": command{0, []string{}, "delete current chat from chat list", nil}, + "members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)", nil}, } var transportConfigurationOptions = map[string]configurationOption{ @@ -112,6 +116,7 @@ type command struct { RequiredArgs int Arguments []string Description string + NotFor *[]ChatType } type configurationOption struct { arguments string @@ -185,14 +190,31 @@ func CommandToHelpString(name string, cmd command) string { return str.String() } -func helpString(typ CommandType) string { +// IsCommandFor checks the suitability of a command for a chat type +func IsCommandForChatType(cmd command, chatType ChatType) bool { + if cmd.NotFor != nil { + for _, typ := range *cmd.NotFor { + if chatType == typ { + return false + } + } + } + + return true +} + +func (c *Client) helpString(typ CommandType, chatId int64) string { var str strings.Builder commandMap := GetCommands(typ) + chatType, chatTypeErr := c.GetChatType(chatId) str.WriteString("Available commands:\n") for _, name := range SortedCommandKeys(commandMap) { command := commandMap[name] + if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { + continue + } str.WriteString(CommandToHelpString(name, command)) str.WriteString("\n") } @@ -502,7 +524,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string case "channel": return c.cmdChannel(args, cmdline) case "help": - return helpString(CommandTypeTransport) + return c.helpString(CommandTypeTransport, 0) } return "" @@ -524,6 +546,11 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return notEnoughArguments, true } + chatType, chatTypeErr := c.GetChatType(chatID) + if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { + return "Not applicable for this chat type", true + } + switch cmd { // delete message case "d": @@ -1103,7 +1130,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return strings.Join(entries, "\n"), true case "help": - return helpString(CommandTypeChat), true + return c.helpString(CommandTypeChat, chatID), true default: return "", false } diff --git a/telegram/utils.go b/telegram/utils.go index 7ab5765..2c8b00d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -53,6 +53,18 @@ var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" const messageHeaderSeparator string = " | " // no hrunicode allowed here yet +// ChatType is an enum of chat types, roughly corresponding to TDLib's one but better +type ChatType int + +const ( + ChatTypeUnknown ChatType = iota + ChatTypePrivate + ChatTypeBasicGroup + ChatTypeSupergroup + ChatTypeSecret + ChatTypeChannel +) + // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { if !c.Online() { @@ -130,10 +142,10 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli return chat, user, nil } -// IsPM checks if a chat is PM -func (c *Client) IsPM(id int64) (bool, error) { +// GetChatType obtains chat type from its information +func (c *Client) GetChatType(id int64) (ChatType, error) { if !c.Online() || id == 0 { - return false, errOffline + return ChatTypeUnknown, errOffline } var err error @@ -144,14 +156,38 @@ func (c *Client) IsPM(id int64) (bool, error) { ChatId: id, }) if err != nil { - return false, err + return ChatTypeUnknown, err } c.cache.SetChat(id, chat) } chatType := chat.Type.ChatTypeType() - if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret { + if chatType == client.TypeChatTypePrivate { + return ChatTypePrivate, nil + } else if chatType == client.TypeChatTypeBasicGroup { + return ChatTypeBasicGroup, nil + } else if chatType == client.TypeChatTypeSupergroup { + supergroup, _ := chat.Type.(*client.ChatTypeSupergroup) + if supergroup.IsChannel { + return ChatTypeChannel, nil + } + return ChatTypeSupergroup, nil + } else if chatType == client.TypeChatTypeSecret { + return ChatTypeSecret, nil + } + + return ChatTypeUnknown, errors.New("Unknown chat type") +} + +// IsPM checks if a chat is PM +func (c *Client) IsPM(id int64) (bool, error) { + typ, err := c.GetChatType(id) + if err != nil { + return false, err + } + + if typ == ChatTypePrivate || typ == ChatTypeSecret { return true, nil } return false, nil diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c50dd1c..3554394 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -475,6 +475,21 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { _ = gateway.ResumableSend(component, &answer) } +func getTelegramChatType(from string, to string) (telegram.ChatType, error) { + toId, ok := toToID(to) + if ok { + bare, _, ok := gateway.SplitJID(from) + if ok { + session, ok := sessions[bare] + if ok { + return session.GetChatType(toId) + } + } + } + + return telegram.ChatTypeUnknown, errors.New("Unknown chat type") +} + func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, @@ -501,6 +516,8 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { } disco.AddFeatures(NSCommand) } else { + chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) + var cmdType telegram.CommandType if ok { cmdType = telegram.CommandTypeChat @@ -510,6 +527,9 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { for name, command := range telegram.GetCommands(cmdType) { if di.Node == name { + if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) { + break + } answer.Payload = di di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node") di.AddFeatures(NSCommand, "jabber:x:data") @@ -549,6 +569,8 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { if di.Node == NSCommand { answer.Payload = di + chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) + var cmdType telegram.CommandType if ok { cmdType = telegram.CommandTypeChat @@ -559,6 +581,9 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { commands := telegram.GetCommands(cmdType) for _, name := range telegram.SortedCommandKeys(commands) { command := commands[name] + if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) { + continue + } di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) } } else { From 0b1cbda1cc20361b90846b6e8534e016288c301f Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 18 Feb 2024 02:48:02 -0500 Subject: [PATCH 27/54] Show member dropdowns in chat administration forms --- telegram/commands.go | 26 +++------ telegram/utils.go | 129 ++++++++++++++++++++++++++++++++++------- telegram/utils_test.go | 6 +- xmpp/handlers.go | 55 +++++++++++++++--- 4 files changed, 165 insertions(+), 51 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index a01a80e..3c899ed 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -70,6 +70,7 @@ var transportCommands = map[string]command{ var notForGroups = []ChatType{ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} var notForPM = []ChatType{ChatTypePrivate, ChatTypeSecret} +var notForPMAndBasic = []ChatType{ChatTypePrivate, ChatTypeSecret, ChatTypeBasicGroup} var onlyForSecret = []ChatType{ChatTypePrivate, ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} var chatCommands = map[string]command{ @@ -93,8 +94,8 @@ var chatCommands = map[string]command{ "invite": command{1, []string{"id or @username"}, "add user to current chat", ¬ForPM}, "link": command{0, []string{}, "get invite link for current chat", ¬ForPM}, "kick": command{1, []string{"id or @username"}, "remove user from current chat", ¬ForPM}, - "mute": command{1, []string{"id or @username", "hours"}, "mute user in current chat", ¬ForPM}, - "unmute": command{1, []string{"id or @username"}, "unrestrict user from current chat", ¬ForPM}, + "mute": command{1, []string{"id or @username", "hours"}, "mute user in current chat", ¬ForPMAndBasic}, + "unmute": command{1, []string{"id or @username"}, "unrestrict user from current chat", ¬ForPMAndBasic}, "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", ¬ForPM}, "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", ¬ForPM}, "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", ¬ForPM}, @@ -1100,30 +1101,17 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) query = args[0] } - members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ - ChatId: chatID, - Limit: 9999, - Query: query, - Filter: &client.ChatMembersFilterMembers{}, - }) + members, err := c.GetChatMembers(chatID, false, query, MembersListMembers) if err != nil { return err.Error(), true } var entries []string - for _, member := range members.Members { - var senderId int64 - switch member.MemberId.MessageSenderType() { - case client.TypeMessageSenderUser: - memberUser, _ := member.MemberId.(*client.MessageSenderUser) - senderId = memberUser.UserId - case client.TypeMessageSenderChat: - memberChat, _ := member.MemberId.(*client.MessageSenderChat) - senderId = memberChat.ChatId - } + for _, member := range members { + senderId := c.GetSenderId(member.MemberId) entries = append(entries, fmt.Sprintf( "%v | role: %v", - c.formatContact(senderId), + c.FormatContact(senderId), member.Status.ChatMemberStatusType(), )) } diff --git a/telegram/utils.go b/telegram/utils.go index 2c8b00d..e7d16d6 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -46,6 +46,7 @@ type messageStub struct { } var errOffline = errors.New("TDlib instance is offline") +var errOverLimit = errors.New("Over limit") var spaceRegex = regexp.MustCompile(`\s+`) var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") @@ -65,6 +66,16 @@ const ( ChatTypeChannel ) +// MembersList is an enum of member list filters +type MembersList int + +const ( + MembersListMembers MembersList = iota + MembersListRestricted + MembersListBanned + MembersListBannedAndAdministrators +) + // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { if !c.Online() { @@ -330,7 +341,8 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o return c.sendPresence(newArgs...) } -func (c *Client) formatContact(chatID int64) string { +// FormatContact retrieves a complete "full name (@usernames)" string for display +func (c *Client) FormatContact(chatID int64) string { if chatID == 0 { return "" } @@ -362,23 +374,27 @@ func (c *Client) formatContact(chatID int64) string { return str } -func (c *Client) getSenderId(message *client.Message) (senderId int64) { - if message.SenderId != nil { - switch message.SenderId.MessageSenderType() { - case client.TypeMessageSenderUser: - senderUser, _ := message.SenderId.(*client.MessageSenderUser) - senderId = senderUser.UserId - case client.TypeMessageSenderChat: - senderChat, _ := message.SenderId.(*client.MessageSenderChat) - senderId = senderChat.ChatId - } +func (c *Client) GetSenderId(sender client.MessageSender) (senderId int64) { + switch sender.MessageSenderType() { + case client.TypeMessageSenderUser: + senderUser, _ := sender.(*client.MessageSenderUser) + senderId = senderUser.UserId + case client.TypeMessageSenderChat: + senderChat, _ := sender.(*client.MessageSenderChat) + senderId = senderChat.ChatId } + return +} +func (c *Client) getMessageSenderId(message *client.Message) (senderId int64) { + if message.SenderId != nil { + senderId = c.GetSenderId(message.SenderId) + } return } func (c *Client) formatSender(message *client.Message) string { - return c.formatContact(c.getSenderId(message)) + return c.FormatContact(c.getMessageSenderId(message)) } func (c *Client) messageToStub(message *client.Message, preview bool, text string) *messageStub { @@ -428,7 +444,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten } gatewayReply = &gateway.Reply{ - Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), + Author: fmt.Sprintf("%v@%s", c.getMessageSenderId(replyMsg), gateway.Jid.Full()), Id: replyId, } } else if !noContent { @@ -445,7 +461,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten } tgReply = &messageStub{ - Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.formatContact(replyTo.ChatId), + Sender: c.formatOrigin(replyTo.Origin) + " @ " + c.FormatContact(replyTo.ChatId), Date: replyTo.OriginSendDate, Text: text, } @@ -515,14 +531,14 @@ func (c *Client) formatOrigin(origin client.MessageOrigin) string { switch origin.MessageOriginType() { case client.TypeMessageOriginUser: originUser := origin.(*client.MessageOriginUser) - return c.formatContact(originUser.SenderUserId) + return c.FormatContact(originUser.SenderUserId) case client.TypeMessageOriginChat: originChat := origin.(*client.MessageOriginChat) var signature string if originChat.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", originChat.AuthorSignature) } - return c.formatContact(originChat.SenderChatId) + signature + return c.FormatContact(originChat.SenderChatId) + signature case client.TypeMessageOriginHiddenUser: originUser := origin.(*client.MessageOriginHiddenUser) return originUser.SenderName @@ -532,7 +548,7 @@ func (c *Client) formatOrigin(origin client.MessageOrigin) string { if channel.AuthorSignature != "" { signature = fmt.Sprintf(" (%s)", channel.AuthorSignature) } - return c.formatContact(channel.ChatId) + signature + return c.FormatContact(channel.ChatId) + signature } return "Unknown origin type" } @@ -701,13 +717,13 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 text := "invited " if len(addMembers.MemberUserIds) > 0 { - text += c.formatContact(addMembers.MemberUserIds[0]) + text += c.FormatContact(addMembers.MemberUserIds[0]) } return text case client.TypeMessageChatDeleteMember: deleteMember, _ := content.(*client.MessageChatDeleteMember) - return "kicked " + c.formatContact(deleteMember.UserId) + return "kicked " + c.FormatContact(deleteMember.UserId) case client.TypeMessagePinMessage: pinMessage, _ := content.(*client.MessagePinMessage) return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, nil) @@ -857,7 +873,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 } case client.TypeMessageChatSetMessageAutoDeleteTime: ttl, _ := content.(*client.MessageChatSetMessageAutoDeleteTime) - name := c.formatContact(ttl.FromUserId) + name := c.FormatContact(ttl.FromUserId) if name == "" { if ttl.MessageAutoDeleteTime == 0 { return "The self-destruct timer was disabled" @@ -1654,3 +1670,76 @@ func (c *Client) usernamesToString(usernames []string) string { } return strings.Join(atUsernames, ", ") } + +// GetChatMembers retrieves a list of chat members. "Limited" mode works only if there are no more than 20 members at all +func (c *Client) GetChatMembers(chatID int64, limited bool, query string, membersList MembersList) ([]*client.ChatMember, error) { + var filters []client.ChatMembersFilter + switch membersList { + case MembersListMembers: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterMembers{}} + case MembersListRestricted: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterRestricted{}} + case MembersListBanned: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}} + case MembersListBannedAndAdministrators: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}, &client.ChatMembersFilterAdministrators{}} + } + + limit := int32(9999) + if limited { + limit = 20 + + chat, _, err := c.GetContactByID(chatID, nil) + if err != nil { + return nil, err + } else if chat == nil { + return nil, errors.New("Chat not found") + } + + chatType := chat.Type.ChatTypeType() + if chatType == client.TypeChatTypeBasicGroup { + basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) + fullInfo, err := c.client.GetBasicGroupFullInfo(&client.GetBasicGroupFullInfoRequest{ + BasicGroupId: basicGroupType.BasicGroupId, + }) + if err != nil { + return nil, err + } + + if len(fullInfo.Members) > int(limit) { + return nil, errOverLimit + } + + return fullInfo.Members, nil + } else if chatType == client.TypeChatTypeSupergroup { + supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) + fullInfo, err := c.client.GetSupergroupFullInfo(&client.GetSupergroupFullInfoRequest{ + SupergroupId: supergroupType.SupergroupId, + }) + if err != nil { + return nil, err + } + + if fullInfo.MemberCount > limit { + return nil, errOverLimit + } + } else { + return nil, errors.New("Inapplicable chat type") + } + } + + var members []*client.ChatMember + for _, filter := range filters { + chatMembers, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ + ChatId: chatID, + Limit: limit, + Query: query, + Filter: filter, + }) + if err != nil { + return nil, err + } + members = append(members, chatMembers.Members...) + } + return members, nil +} diff --git a/telegram/utils_test.go b/telegram/utils_test.go index fa9c107..a0939cd 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -567,7 +567,7 @@ func TestMessageToPrefix7(t *testing.T) { func GetSenderIdEmpty(t *testing.T) { message := client.Message{} - senderId := (&Client{}).getSenderId(&message) + senderId := (&Client{}).getMessageSenderId(&message) if senderId != 0 { t.Errorf("Wrong sender id: %v", senderId) } @@ -579,7 +579,7 @@ func GetSenderIdUser(t *testing.T) { UserId: 42, }, } - senderId := (&Client{}).getSenderId(&message) + senderId := (&Client{}).getMessageSenderId(&message) if senderId != 42 { t.Errorf("Wrong sender id: %v", senderId) } @@ -591,7 +591,7 @@ func GetSenderIdChat(t *testing.T) { ChatId: -42, }, } - senderId := (&Client{}).getSenderId(&message) + senderId := (&Client{}).getMessageSenderId(&message) if senderId != -42 { t.Errorf("Wrong sender id: %v", senderId) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 3554394..945f119 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -790,23 +790,59 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command dummyString := "" required = &dummyString } - fields = append(fields, &stanza.Field{ + + var fieldType string + var options []stanza.Option + if toOk && i == 0 { + switch command.Node { + case "mute", "kick", "ban", "promote", "unmute", "unban": + session, ok := sessions[bare] + if ok { + var membersList telegram.MembersList + switch command.Node { + case "unmute": + membersList = telegram.MembersListRestricted + case "unban": + membersList = telegram.MembersListBannedAndAdministrators + } + members, err := session.GetChatMembers(toId, true, "", membersList) + if err == nil { + fieldType = stanza.FieldTypeListSingle + for _, member := range members { + senderId := session.GetSenderId(member.MemberId) + options = append(options, stanza.Option{ + Label: session.FormatContact(senderId), + ValuesList: []string{strconv.FormatInt(senderId, 10)}, + }) + } + } + } + } + } + + field := stanza.Field{ Var: strconv.FormatInt(int64(i), 10), Label: arg, Required: required, - }) + Type: fieldType, + Options: options, + } + fields = append(fields, &field) + log.Debugf("field: %#v", field) + } + form := stanza.Form{ + Type: stanza.FormTypeForm, + Title: command.Node, + Instructions: []string{cmd.Description}, + Fields: fields, } answer.Payload = &stanza.Command{ SessionId: command.Node, Node: command.Node, Status: stanza.CommandStatusExecuting, - CommandElement: &stanza.Form{ - Type: stanza.FormTypeForm, - Title: command.Node, - Instructions: []string{cmd.Description}, - Fields: fields, - }, + CommandElement: &form, } + log.Debugf("form: %#v", form) } else { cmdString = "/" + command.Node } @@ -842,8 +878,9 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command }, } - log.Debugf("command response: %#v", answer.Payload) } + + log.Debugf("command response: %#v", answer.Payload) } func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { From 5dd60450c28e602865e94bd28898eb4e61594a54 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 18 Feb 2024 02:48:57 -0500 Subject: [PATCH 28/54] Fix crashes in commands due to not found contacts --- telegram/commands.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/telegram/commands.go b/telegram/commands.go index 3c899ed..596f4a9 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -504,6 +504,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string if err != nil { return err.Error() } + if contact == nil { + return "Contact not found" + } text := rawCmdArguments(cmdline, 1) _, err = c.client.ReportChat(&client.ReportChatRequest{ @@ -812,6 +815,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if contact == nil { + return "Contact not found", true + } _, err = c.client.AddChatMember(&client.AddChatMemberRequest{ ChatId: chatID, @@ -836,6 +842,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if contact == nil { + return "Contact not found", true + } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ ChatId: chatID, @@ -851,6 +860,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if contact == nil { + return "Contact not found", true + } var hours int64 if len(args) > 1 { @@ -878,6 +890,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if contact == nil { + return "Contact not found", true + } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ ChatId: chatID, @@ -897,6 +912,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if contact == nil { + return "Contact not found", true + } var hours int64 if len(args) > 1 { @@ -922,6 +940,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if contact == nil { + return "Contact not found", true + } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ ChatId: chatID, @@ -937,6 +958,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if contact == nil { + return "Contact not found", true + } // clone the permissions status := client.ChatMemberStatusAdministrator{ @@ -1006,6 +1030,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } + if chat == nil { + return "Chat not found", true + } chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypeSecret { From 154b59de44d305c17b4e0228e62eca0a408558ed Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 18 Feb 2024 04:36:23 -0500 Subject: [PATCH 29/54] Show command execution success status --- telegram/commands.go | 264 ++++++++++++++++++++++--------------------- telegram/utils.go | 2 +- xmpp/handlers.go | 16 ++- 3 files changed, 147 insertions(+), 135 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 596f4a9..0200e05 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -308,27 +308,27 @@ func (c *Client) usernameOrIDToID(username string) (int64, error) { } // ProcessTransportCommand executes a command sent directly to the component -// and returns a response -func (c *Client) ProcessTransportCommand(cmdline string, resource string) string { +// and returns a response and execution success result +func (c *Client) ProcessTransportCommand(cmdline string, resource string) (string, bool) { cmd, args := parseCommand(cmdline) command, ok := transportCommands[cmd] if !ok { - return unknownCommand + return unknownCommand, false } if len(args) < command.RequiredArgs { - return notEnoughArguments + return notEnoughArguments, false } switch cmd { case "login", "code", "password": if cmd == "login" && c.Session.Login != "" { - return "Phone number already provided, use /cancelauth to start over" + return "Phone number already provided, use /cancelauth to start over", false } if cmd == "login" { err := c.TryLogin(resource, args[0]) if err != nil { - return err.Error() + return err.Error(), false } c.locks.authorizerWriteLock.Lock() @@ -340,11 +340,11 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string defer c.locks.authorizerWriteLock.Unlock() if c.authorizer == nil { - return TelegramNotInitialized + return TelegramNotInitialized, false } if c.authorizer.isClosed { - return TelegramAuthDone + return TelegramAuthDone, false } switch cmd { @@ -359,7 +359,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string // sign out case "logout": if !c.Online() { - return notOnline + return notOnline, false } for _, id := range c.cache.ChatsKeys() { @@ -369,21 +369,21 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string _, err := c.client.LogOut() if err != nil { c.forceClose() - return errors.Wrap(err, "Logout error").Error() + return errors.Wrap(err, "Logout error").Error(), false } c.Session.Login = "" // cancel auth case "cancelauth": if c.Online() { - return "Not allowed when online, use /logout instead" + return "Not allowed when online, use /logout instead", false } c.cancelAuth() - return "Cancelled" + return "Cancelled", true // set @username case "setusername": if !c.Online() { - return notOnline + return notOnline, false } var username string @@ -395,7 +395,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string Username: username, }) if err != nil { - return errors.Wrap(err, "Couldn't set username").Error() + return errors.Wrap(err, "Couldn't set username").Error(), false } // set My Name case "setname": @@ -403,7 +403,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string var lastname string if firstname == "" { - return "The name should contain at least one character" + return "The name should contain at least one character", false } if len(args) > 1 { lastname = rawCmdArguments(cmdline, 1) @@ -417,7 +417,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } else { c.locks.authorizerWriteLock.Unlock() if !c.Online() { - return notOnline + return notOnline, false } _, err := c.client.SetName(&client.SetNameRequest{ @@ -425,25 +425,25 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string LastName: lastname, }) if err != nil { - return errors.Wrap(err, "Couldn't set name").Error() + return errors.Wrap(err, "Couldn't set name").Error(), false } } // set About case "setbio": if !c.Online() { - return notOnline + return notOnline, false } _, err := c.client.SetBio(&client.SetBioRequest{ Bio: rawCmdArguments(cmdline, 0), }) if err != nil { - return errors.Wrap(err, "Couldn't set bio").Error() + return errors.Wrap(err, "Couldn't set bio").Error(), false } // set password case "setpassword": if !c.Online() { - return notOnline + return notOnline, false } var oldPassword string @@ -458,39 +458,39 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string NewPassword: newPassword, }) if err != nil { - return errors.Wrap(err, "Couldn't set password").Error() + return errors.Wrap(err, "Couldn't set password").Error(), false } case "config": if len(args) > 1 { var msg string if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { - return "The server did not allow to enable carbons" + return "The server did not allow to enable carbons", false } if !c.Session.RawMessages && args[0] == "nativeedits" && args[1] == "true" { - return "nativeedits only works with rawmessages as of yet, enable it first" + return "nativeedits only works with rawmessages as of yet, enable it first", false } if c.Session.NativeEdits && args[0] == "rawmessages" && args[1] == "false" { _, err := c.Session.Set("nativeedits", "false") if err != nil { - return err.Error() + return err.Error(), false } msg = "Automatically disabling nativeedits too...\n" } value, err := c.Session.Set(args[0], args[1]) if err != nil { - return err.Error() + return err.Error(), false } gateway.DirtySessions = true - return fmt.Sprintf("%s%s set to %s", msg, args[0], value) + return fmt.Sprintf("%s%s set to %s", msg, args[0], value), true } else if len(args) > 0 { value, err := c.Session.Get(args[0]) if err != nil { - return err.Error() + return err.Error(), false } - return fmt.Sprintf("%s is set to %s", args[0], value) + return fmt.Sprintf("%s is set to %s", args[0], value), true } var entries []string @@ -498,14 +498,14 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string entries = append(entries, fmt.Sprintf("%s is set to %s", key, value)) } - return strings.Join(entries, "\n") + return strings.Join(entries, "\n"), true case "report": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error() + return err.Error(), false } if contact == nil { - return "Contact not found" + return "Contact not found", false } text := rawCmdArguments(cmdline, 1) @@ -515,9 +515,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string Text: text, }) if err != nil { - return err.Error() + return err.Error(), false } else { - return "Reported" + return "Reported", true } case "add": return c.cmdAdd(args) @@ -528,45 +528,45 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string case "channel": return c.cmdChannel(args, cmdline) case "help": - return c.helpString(CommandTypeTransport, 0) + return c.helpString(CommandTypeTransport, 0), true } - return "" + return "", true } // ProcessChatCommand executes a command sent in a mapped chat -// and returns a response and the status of command support -func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) { +// and returns a response, the status of command support and the execution success result +func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, bool) { if !c.Online() { - return notOnline, true + return notOnline, true, false } cmd, args := parseCommand(cmdline) command, ok := chatCommands[cmd] if !ok { - return unknownCommand, false + return unknownCommand, false, false } if len(args) < command.RequiredArgs { - return notEnoughArguments, true + return notEnoughArguments, true, false } chatType, chatTypeErr := c.GetChatType(chatID) if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { - return "Not applicable for this chat type", true + return "Not applicable for this chat type", true, false } switch cmd { // delete message case "d": if c.me == nil { - return "@me is not initialized", true + return "@me is not initialized", true, false } var limit int32 if len(args) > 0 { limit64, err := strconv.ParseInt(args[0], 10, 32) if err != nil { - return err.Error(), true + return err.Error(), true, false } limit = int32(limit64) } else { @@ -575,7 +575,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) messages, err := c.getLastMessages(chatID, "", c.me.Id, limit) if err != nil { - return err.Error(), true + return err.Error(), true, false } log.Debugf("pre-deletion query: %#v %#v", messages, messages.Messages) @@ -592,25 +592,25 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Revoke: true, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // edit message case "s": if c.me == nil { - return "@me is not initialized", true + return "@me is not initialized", true, false } messages, err := c.getLastMessages(chatID, "", c.me.Id, 1) if err != nil { - return err.Error(), true + return err.Error(), true, false } if len(messages.Messages) == 0 { - return "No last message", true + return "No last message", true, false } message := messages.Messages[0] if message == nil { - return "Last message is empty", true + return "Last message is empty", true, false } content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) @@ -622,10 +622,10 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) InputMessageContent: content, }) if err != nil { - return "Message editing error", true + return "Message editing error", true, false } } else { - return "Message processing error", true + return "Message processing error", true, false } // send without sound case "silent": @@ -640,10 +640,10 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } } else { - return "Message processing error", true + return "Message processing error", true, false } // schedule a message to timestamp or to going online case "schedule": @@ -700,7 +700,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) break } - return "Invalid schedule time specifier", true + return "Invalid schedule time specifier", true, false } } @@ -715,23 +715,23 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } - return "Scheduled to " + result, true + return "Scheduled to " + result, true, true } else { - return "Message processing error", true + return "Message processing error", true, false } // forward a message to chat case "forward": messageId, err := strconv.ParseInt(args[0], 10, 64) if err != nil { - return "Cannot parse message ID", true + return "Cannot parse message ID", true, false } targetChatParts := strings.Split(args[1], "@") // full JIDs are supported too targetChatId, err := strconv.ParseInt(targetChatParts[0], 10, 64) if err != nil { - return "Cannot parse target chat ID", true + return "Cannot parse target chat ID", true, false } messages, err := c.client.ForwardMessages(&client.ForwardMessagesRequest{ @@ -740,7 +740,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) MessageIds: []int64{messageId}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } if messages != nil && messages.Messages != nil { for _, message := range messages.Messages { @@ -751,7 +751,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) case "vcard": info, err := c.GetVcardInfo(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } _, link := c.PermastoreFile(info.Photo, true) entries := []string{ @@ -761,26 +761,30 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) keyValueString("Full name", info.Given+" "+info.Family), keyValueString("Phone number", info.Tel), } - return strings.Join(entries, "\n"), true + return strings.Join(entries, "\n"), true, true // add @contact case "add": - return c.cmdAdd(args), true + response, success := c.cmdAdd(args) + return response, true, success // join https://t.me/publichat or @publicchat case "join": - return c.cmdJoin(args), true + response, success := c.cmdJoin(args) + return response, true, success // create new supergroup case "supergroup": - return c.cmdSupergroup(args, cmdline), true + response, success := c.cmdSupergroup(args, cmdline) + return response, true, success // create new channel case "channel": - return c.cmdChannel(args, cmdline), true + response, success := c.cmdChannel(args, cmdline) + return response, true, success // create new secret chat with current user case "secret": _, err := c.client.CreateNewSecretChat(&client.CreateNewSecretChatRequest{ UserId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // create group chat with current user case "group": @@ -789,7 +793,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Title: args[0], }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // blacklists current user case "block": @@ -798,7 +802,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) BlockList: &client.BlockListMain{}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // unblacklists current user case "unblock": @@ -807,16 +811,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) BlockList: nil, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // invite @username to current groupchat case "invite": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false } if contact == nil { - return "Contact not found", true + return "Contact not found", true, false } _, err = c.client.AddChatMember(&client.AddChatMemberRequest{ @@ -825,7 +829,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ForwardLimit: 100, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // get link to current chat case "link": @@ -833,17 +837,17 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ChatId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } - return link.InviteLink, true + return link.InviteLink, true, true // kick @username from current group chat case "kick": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false } if contact == nil { - return "Contact not found", true + return "Contact not found", true, false } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ @@ -852,23 +856,23 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Status: &client.ChatMemberStatusLeft{}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // mute @username [n hours] case "mute": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false } if contact == nil { - return "Contact not found", true + return "Contact not found", true, false } var hours int64 if len(args) > 1 { hours, err = strconv.ParseInt(args[1], 10, 32) if err != nil { - return "Invalid number of hours", true + return "Invalid number of hours", true, false } } @@ -882,16 +886,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // unmute @username case "unmute": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false } if contact == nil { - return "Contact not found", true + return "Contact not found", true, false } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ @@ -904,23 +908,23 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // ban @username from current chat [for N hours] case "ban": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false } if contact == nil { - return "Contact not found", true + return "Contact not found", true, false } var hours int64 if len(args) > 1 { hours, err = strconv.ParseInt(args[1], 10, 32) if err != nil { - return "Invalid number of hours", true + return "Invalid number of hours", true, false } } @@ -932,16 +936,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // unban @username case "unban": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false } if contact == nil { - return "Contact not found", true + return "Contact not found", true, false } _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ @@ -950,16 +954,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Status: &client.ChatMemberStatusMember{}, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // promote @username to admin case "promote": contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return err.Error(), true + return err.Error(), true, false } if contact == nil { - return "Contact not found", true + return "Contact not found", true, false } // clone the permissions @@ -978,7 +982,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Status: &status, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // leave current chat case "leave": @@ -986,12 +990,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ChatId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } // leave current chat (for owners) case "leave!": @@ -999,12 +1003,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) ChatId: chatID, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } // set TTL case "ttl": @@ -1013,7 +1017,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if len(args) > 0 { ttl, err = strconv.ParseInt(args[0], 10, 32) if err != nil { - return "Invalid TTL", true + return "Invalid TTL", true, false } } _, err = c.client.SetChatMessageAutoDeleteTime(&client.SetChatMessageAutoDeleteTimeRequest{ @@ -1022,16 +1026,16 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) }) if err != nil { - return err.Error(), true + return err.Error(), true, false } // close secret chat case "close": chat, _, err := c.GetContactByID(chatID, nil) if err != nil { - return err.Error(), true + return err.Error(), true, false } if chat == nil { - return "Chat not found", true + return "Chat not found", true, false } chatType := chat.Type.ChatTypeType() @@ -1041,12 +1045,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) SecretChatId: chatTypeSecret.SecretChatId, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } } // delete current chat @@ -1057,12 +1061,12 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Revoke: true, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } err = c.unsubscribe(chatID) if err != nil { - return err.Error(), true + return err.Error(), true, false } // message search case "search": @@ -1081,7 +1085,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) messages, err := c.getLastMessages(chatID, query, 0, limit) if err != nil { - return err.Error(), true + return err.Error(), true, false } c.sendMessagesReverse(chatID, messages.Messages) @@ -1110,7 +1114,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) Limit: limit, }) if err != nil { - return err.Error(), true + return err.Error(), true, false } messages = append(messages, newMessages.Messages...) @@ -1130,7 +1134,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) members, err := c.GetChatMembers(chatID, false, query, MembersListMembers) if err != nil { - return err.Error(), true + return err.Error(), true, false } var entries []string @@ -1143,82 +1147,82 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) )) } - return strings.Join(entries, "\n"), true + return strings.Join(entries, "\n"), true, true case "help": - return c.helpString(CommandTypeChat, chatID), true + return c.helpString(CommandTypeChat, chatID), true, true default: - return "", false + return "", false, false } - return "", true + return "", true, true } -func (c *Client) cmdAdd(args []string) string { +func (c *Client) cmdAdd(args []string) (string, bool) { chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: args[0], }) if err != nil { - return err.Error() + return err.Error(), false } if chat == nil { - return "No error, but chat is nil" + return "No error, but chat is nil", false } c.subscribeToID(chat.Id, chat) - return "" + return "", true } -func (c *Client) cmdJoin(args []string) string { +func (c *Client) cmdJoin(args []string) (string, bool) { if strings.HasPrefix(args[0], "@") { chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ Username: args[0], }) if err != nil { - return err.Error() + return err.Error(), false } if chat == nil { - return "No error, but chat is nil" + return "No error, but chat is nil", false } _, err = c.client.JoinChat(&client.JoinChatRequest{ ChatId: chat.Id, }) if err != nil { - return err.Error() + return err.Error(), false } } else { _, err := c.client.JoinChatByInviteLink(&client.JoinChatByInviteLinkRequest{ InviteLink: args[0], }) if err != nil { - return err.Error() + return err.Error(), false } } - return "" + return "", true } -func (c *Client) cmdSupergroup(args []string, cmdline string) string { +func (c *Client) cmdSupergroup(args []string, cmdline string) (string, bool) { _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ Title: args[0], Description: rawCmdArguments(cmdline, 1), }) if err != nil { - return err.Error() + return err.Error(), false } - return "" + return "", true } -func (c *Client) cmdChannel(args []string, cmdline string) string { +func (c *Client) cmdChannel(args []string, cmdline string) (string, bool) { _, err := c.client.CreateNewSupergroupChat(&client.CreateNewSupergroupChatRequest{ Title: args[0], Description: rawCmdArguments(cmdline, 1), IsChannel: true, }) if err != nil { - return err.Error() + return err.Error(), false } - return "" + return "", true } diff --git a/telegram/utils.go b/telegram/utils.go index e7d16d6..5c26b8c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1169,7 +1169,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands - response, isCommand := c.ProcessChatCommand(chatID, text) + response, isCommand, _ := c.ProcessChatCommand(chatID, text) if response != "" { c.returnMessage(returnJid, chatID, response) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 945f119..be53189 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -230,7 +230,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } else { toJid, err := stanza.NewJid(msg.To) if err == nil && toJid.Bare() == gatewayJid && (strings.HasPrefix(msg.Body, "/") || strings.HasPrefix(msg.Body, "!")) { - response := session.ProcessTransportCommand(msg.Body, resource) + response, _ := session.ProcessTransportCommand(msg.Body, resource) if response != "" { gateway.SendServiceMessage(msg.From, response, component) } @@ -862,10 +862,18 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command } var response string + var success bool if toOk { - response, _ = session.ProcessChatCommand(toId, cmdString) + response, _, success = session.ProcessChatCommand(toId, cmdString) } else { - response = session.ProcessTransportCommand(cmdString, resource) + response, success = session.ProcessTransportCommand(cmdString, resource) + } + + var noteType string + if success { + noteType = stanza.CommandNoteTypeInfo + } else { + noteType = stanza.CommandNoteTypeErr } answer.Payload = &stanza.Command{ @@ -874,7 +882,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command Status: stanza.CommandStatusCompleted, CommandElement: &stanza.Note{ Text: response, - Type: stanza.CommandNoteTypeInfo, + Type: noteType, }, } From 67c38823f2b053928c6c0a5a13261b891e9db4d3 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 29 Mar 2024 07:35:06 -0400 Subject: [PATCH 30/54] Avoid broken state on a failed logout attempt --- telegram/commands.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 9251ebb..d9dc1f2 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -279,16 +279,15 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string return notOnline } - for _, id := range c.cache.ChatsKeys() { - c.unsubscribe(id) - } - _, err := c.client.LogOut() if err != nil { - c.forceClose() return errors.Wrap(err, "Logout error").Error() } + for _, id := range c.cache.ChatsKeys() { + c.unsubscribe(id) + } + c.Session.Login = "" // cancel auth case "cancelauth": From 908bd76aacfae7ae11b49e83da060232703bfa4f Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 29 Mar 2024 07:39:10 -0400 Subject: [PATCH 31/54] Add staging.Dockerfile --- Dockerfile | 2 +- Makefile | 6 +++++- staging.Dockerfile | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 staging.Dockerfile diff --git a/Dockerfile b/Dockerfile index 6fea570..c3858e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ WORKDIR /src RUN make ${MAKEOPTS} FROM scratch AS telegabber -COPY --from=build /src/telegabber /usr/local/bin/ +COPY --from=build /src/release/telegabber /usr/local/bin/ ENTRYPOINT ["/usr/local/bin/telegabber"] FROM scratch AS binaries diff --git a/Makefile b/Makefile index c859606..69375be 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,8 @@ VERSION := "v1.9.1" MAKEOPTS := "-j4" all: - go build -ldflags "-X main.commit=${COMMIT}" -o telegabber + mkdir -p release + go build -ldflags "-X main.commit=${COMMIT}" -o release/telegabber test: go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter ./badger @@ -16,3 +17,6 @@ lint: build_indocker: docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "VERSION=${VERSION}" --build-arg "MAKEOPTS=${MAKEOPTS}" --output=release --target binaries . + +build_indocker_staging: + DOCKER_BUILDKIT=1 docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "MAKEOPTS=${MAKEOPTS}" --network host --output=release --target binaries -f staging.Dockerfile . diff --git a/staging.Dockerfile b/staging.Dockerfile new file mode 100644 index 0000000..7b9a91b --- /dev/null +++ b/staging.Dockerfile @@ -0,0 +1,43 @@ +FROM golang:1.19-bullseye AS base + +RUN apt-get update +RUN apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git php + +FROM base AS tdlib + +ARG TD_COMMIT +ARG MAKEOPTS +RUN git clone https://github.com/tdlib/td /src/ +RUN git -C /src/ checkout "${TD_COMMIT}" +RUN mkdir build +WORKDIR /build/ +RUN cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/compiled/ /src/ +RUN cmake --build . --target prepare_cross_compiling ${MAKEOPTS} +WORKDIR /src/ +RUN php SplitSource.php +WORKDIR /build/ +RUN cmake --build . ${MAKEOPTS} +RUN make install + +FROM base AS cache +ARG VERSION +COPY --from=tdlib /compiled/ /usr/local/ +WORKDIR /src +RUN --mount=type=cache,target=/go \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=bind,source=./,target=/src \ + go get + +FROM cache AS build +ARG MAKEOPTS +WORKDIR /src +RUN --mount=type=bind,source=./,target=/src,rw \ + --mount=type=cache,destination=/src/release \ + make ${MAKEOPTS} + +FROM build AS release +RUN --mount=type=cache,destination=/src/release \ + cp /src/release/telegabber / + +FROM scratch AS binaries +COPY --from=release /telegabber / From 3e772be7a6f3312958c0ea0de7eff7a45ece192b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 9 Apr 2024 19:08:37 -0400 Subject: [PATCH 32/54] Add tdlib.Dockerfile --- .gitignore | 1 + Makefile | 3 +++ tdlib.Dockerfile | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 tdlib.Dockerfile diff --git a/.gitignore b/.gitignore index b132b72..cf4df11 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ sessions/ session.dat session.dat.new release/ +tdlib/ diff --git a/Makefile b/Makefile index 69375be..87eee4c 100644 --- a/Makefile +++ b/Makefile @@ -20,3 +20,6 @@ build_indocker: build_indocker_staging: DOCKER_BUILDKIT=1 docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "MAKEOPTS=${MAKEOPTS}" --network host --output=release --target binaries -f staging.Dockerfile . + +build_tdlib: + DOCKER_BUILDKIT=1 docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "MAKEOPTS=${MAKEOPTS}" --output=tdlib --target binaries -f tdlib.Dockerfile . diff --git a/tdlib.Dockerfile b/tdlib.Dockerfile new file mode 100644 index 0000000..5774405 --- /dev/null +++ b/tdlib.Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.19-bullseye AS base + +RUN apt-get update +RUN apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git php + +FROM base AS tdlib + +ARG TD_COMMIT +ARG MAKEOPTS +RUN git clone https://github.com/tdlib/td /src/ +RUN git -C /src/ checkout "${TD_COMMIT}" +RUN mkdir build +WORKDIR /build/ +RUN cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/compiled/ /src/ +RUN cmake --build . --target prepare_cross_compiling ${MAKEOPTS} +WORKDIR /src/ +RUN php SplitSource.php +WORKDIR /build/ +RUN cmake --build . ${MAKEOPTS} +RUN make install + +FROM scratch AS binaries +COPY --from=tdlib /compiled/ / From 144c5724ea7ed4f1a9002e065f517e1403ef4e76 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 9 Apr 2024 19:09:47 -0400 Subject: [PATCH 33/54] Fix module cache in staging.Dockerfile --- staging.Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/staging.Dockerfile b/staging.Dockerfile index 7b9a91b..e9fdd1e 100644 --- a/staging.Dockerfile +++ b/staging.Dockerfile @@ -23,15 +23,18 @@ FROM base AS cache ARG VERSION COPY --from=tdlib /compiled/ /usr/local/ WORKDIR /src -RUN --mount=type=cache,target=/go \ - --mount=type=cache,target=/root/.cache/go-build \ +RUN go env -w GOCACHE=/go-cache +RUN go env -w GOMODCACHE=/gomod-cache +RUN --mount=type=cache,target=/gomod-cache \ --mount=type=bind,source=./,target=/src \ - go get + go mod download FROM cache AS build ARG MAKEOPTS WORKDIR /src RUN --mount=type=bind,source=./,target=/src,rw \ + --mount=type=cache,target=/go-cache \ + --mount=type=cache,target=/gomod-cache \ --mount=type=cache,destination=/src/release \ make ${MAKEOPTS} From b499992148978913aacac2e86248f0b89c6c81b7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 10 Apr 2024 22:17:58 -0400 Subject: [PATCH 34/54] Fix missing read markers in other XMPP clients than the message sender --- telegram/handlers.go | 44 +++++++++++++++++++++----------------------- xmpp/handlers.go | 2 +- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 425309e..ed18e4f 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -56,30 +56,28 @@ func (c *Client) cleanTempFile(path string) { } func (c *Client) sendMarker(chatId, messageId int64, typ gateway.MarkerType) { - if xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, messageId); err == nil { - resource := c.getFromOutbox(xmppId) - - var stringType string - if typ == gateway.MarkerTypeReceived { - stringType = "received" - } else if typ == gateway.MarkerTypeDisplayed { - stringType = "displayed" - } - log.WithFields(log.Fields{ - "xmppId": xmppId, - "resource": resource, - }).Debugf("marker: %s", stringType) - - if resource != "" { - gateway.SendMessageMarker( - c.jid+"/"+resource, - strconv.FormatInt(chatId, 10), - c.xmpp, - typ, - xmppId, - ) - } + xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, messageId) + if err != nil { + xmppId = strconv.FormatInt(messageId, 10) } + + var stringType string + if typ == gateway.MarkerTypeReceived { + stringType = "received" + } else if typ == gateway.MarkerTypeDisplayed { + stringType = "displayed" + } + log.WithFields(log.Fields{ + "xmppId": xmppId, + }).Debugf("marker: %s", stringType) + + gateway.SendMessageMarker( + c.jid, + strconv.FormatInt(chatId, 10), + c.xmpp, + typ, + xmppId, + ) } func (c *Client) updateHandler() { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 8c6ba37..811cef6 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -203,7 +203,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } else { err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) if err == nil { - session.AddToOutbox(msg.Id, resource) + // session.AddToOutbox(msg.Id, resource) session.UpdateLastChatMessageId(toID, msg.Id) } else { log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) From a36856b76852c98681fd7c30fbdededb3cfc5470 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 11 Apr 2024 20:37:51 -0400 Subject: [PATCH 35/54] Fix filtering content updates for outgoing messages --- telegram/handlers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telegram/handlers.go b/telegram/handlers.go index ed18e4f..64280e6 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -371,6 +371,8 @@ func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucc log.Errorf("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) } + c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) + c.sendMarker(update.Message.ChatId, update.Message.Id, gateway.MarkerTypeReceived) // clean uploaded files From f15e44436beb9629bbd57607faba072b1f9a9b18 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 11 Apr 2024 20:59:49 -0400 Subject: [PATCH 36/54] Use carbons for non-native edits too --- telegram/handlers.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 64280e6..3c54746 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -304,20 +304,21 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } if ok && lastXmppId == xmppId { replaceId = xmppId - message, err := c.client.GetMessage(&client.GetMessageRequest{ - ChatId: update.ChatId, - MessageId: update.MessageId, - }) - if err == nil { - isCarbon = c.isCarbonsEnabled() && message.IsOutgoing - } else { - log.Errorf("No message %v/%v found, cannot reliably determine if it's a carbon", update.ChatId, update.MessageId) - } } else { log.Infof("Mismatching message ids: %v %v, falling back to separate edit message", lastXmppId, xmppId) } } + message, err := c.client.GetMessage(&client.GetMessageRequest{ + ChatId: update.ChatId, + MessageId: update.MessageId, + }) + if err == nil { + isCarbon = c.isCarbonsEnabled() && message.IsOutgoing + } else { + log.Errorf("No message %v/%v found, cannot reliably determine if it's a carbon", update.ChatId, update.MessageId) + } + text := formatter.Format( textContent.Text.Text, textContent.Text.Entities, From 2459b14948e71a5db640b63a41720f2b197f3fd4 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 11 Apr 2024 22:24:22 -0400 Subject: [PATCH 37/54] Version 1.9.2 --- Makefile | 2 +- telegabber.go | 2 +- telegram/handlers.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 87eee4c..3b7cd19 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.1" +VERSION := "v1.9.2" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index f42e266..6c10d0d 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.1" +var version string = "1.9.2" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/handlers.go b/telegram/handlers.go index 3c54746..05c12ca 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -68,7 +68,7 @@ func (c *Client) sendMarker(chatId, messageId int64, typ gateway.MarkerType) { stringType = "displayed" } log.WithFields(log.Fields{ - "xmppId": xmppId, + "xmppId": xmppId, }).Debugf("marker: %s", stringType) gateway.SendMessageMarker( From a3f6d5f77402bf4a4d3fa01297f9fd78cc69a3b3 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 27 Apr 2024 00:31:21 -0400 Subject: [PATCH 38/54] Support nativeedits for rawmessages=false --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 10 ------- telegram/handlers.go | 31 ++++++++++++++-------- telegram/utils.go | 60 ++++++++++++++++++++++++------------------ telegram/utils_test.go | 40 +++++++++++++++++++++++----- 6 files changed, 90 insertions(+), 55 deletions(-) diff --git a/Makefile b/Makefile index 3b7cd19..a90eb8f 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.2" +VERSION := "v1.9.3" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 6c10d0d..c39d91d 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.2" +var version string = "1.9.3" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index d9dc1f2..1ce316b 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -384,16 +384,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { return "The server did not allow to enable carbons" } - if !c.Session.RawMessages && args[0] == "nativeedits" && args[1] == "true" { - return "nativeedits only works with rawmessages as of yet, enable it first" - } - if c.Session.NativeEdits && args[0] == "rawmessages" && args[1] == "false" { - _, err := c.Session.Set("nativeedits", "false") - if err != nil { - return err.Error() - } - msg = "Automatically disabling nativeedits too...\n" - } value, err := c.Session.Set(args[0], args[1]) if err != nil { diff --git a/telegram/handlers.go b/telegram/handlers.go index 05c12ca..6266292 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -309,34 +309,43 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } } - message, err := c.client.GetMessage(&client.GetMessageRequest{ + message, messageErr := c.client.GetMessage(&client.GetMessageRequest{ ChatId: update.ChatId, MessageId: update.MessageId, }) - if err == nil { + var prefix string + if messageErr == nil { isCarbon = c.isCarbonsEnabled() && message.IsOutgoing + // reply correction support in clients is suboptimal yet, so cut them out for now + prefix, _ = c.messageToPrefix(message, "", "", true) } else { log.Errorf("No message %v/%v found, cannot reliably determine if it's a carbon", update.ChatId, update.MessageId) } - text := formatter.Format( - textContent.Text.Text, - textContent.Text.Entities, - markupFunction, - ) + var text strings.Builder if replaceId == "" { var editChar string if c.Session.AsciiArrows { - editChar = "e " + editChar = "e" } else { - editChar = "✎ " + editChar = "✎" } - text = editChar + fmt.Sprintf("%v | %s", update.MessageId, text) + text.WriteString(fmt.Sprintf("%s %v | ", editChar, update.MessageId)) + } else if prefix != "" { + text.WriteString(prefix) + text.WriteString(c.getPrefixSeparator(update.ChatId)) } + text.WriteString(formatter.Format( + textContent.Text.Text, + textContent.Text.Entities, + markupFunction, + )) + + sChatId := strconv.FormatInt(update.ChatId, 10) for _, jid := range jids { - gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+sId, c.xmpp, nil, replaceId, isCarbon, false) + gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false) } } } diff --git a/telegram/utils.go b/telegram/utils.go index 7ab5765..4509d1a 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -915,7 +915,7 @@ func (c *Client) isCarbonsEnabled() bool { return gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons } -func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string) (string, *gateway.Reply) { +func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, suppressReply bool) (string, *gateway.Reply) { isPM, err := c.IsPM(message.ChatId) if err != nil { log.Errorf("Could not determine if chat is PM: %v", err) @@ -953,27 +953,32 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } // reply to - preview := true - reply, tgReply := c.getMessageReply(message, preview, false) + var reply *gateway.Reply + if !suppressReply { + preview := true + gwReply, tgReply := c.getMessageReply(message, preview, false) - if tgReply != nil { - var replyStart, replyEnd int + if tgReply != nil { + reply = gwReply - if len(prefix) > 0 { - replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) - } + var replyStart, replyEnd int - replyLine := "reply: " + c.formatMessageContent(preview, tgReply) - prefix = append(prefix, replyLine) + if len(prefix) > 0 { + replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) + } - replyEnd = replyStart + utf8.RuneCountInString(replyLine) - if len(prefix) > 0 { - replyEnd += len(messageHeaderSeparator) - } + replyLine := "reply: " + c.formatMessageContent(preview, tgReply) + prefix = append(prefix, replyLine) - if reply != nil { - reply.Start = uint64(replyStart) - reply.End = uint64(replyEnd) + replyEnd = replyStart + utf8.RuneCountInString(replyLine) + if len(prefix) > 0 { + replyEnd += len(messageHeaderSeparator) + } + + if reply != nil { + reply.Start = uint64(replyStart) + reply.End = uint64(replyEnd) + } } } @@ -1008,6 +1013,17 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File { return file } +// \n if it is groupchat and message is not empty +func (c *Client) getPrefixSeparator(chatId int64) string { + var separator string + if chatId < 0 { + separator = "\n" + } else if chatId > 0 { + separator = " | " + } + return separator +} + // ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { isCarbon := c.isCarbonsEnabled() && message.IsOutgoing @@ -1051,21 +1067,15 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } else if !c.Session.RawMessages { var newText strings.Builder - prefix, prefixReply := c.messageToPrefix(message, previewName, fileName) + prefix, prefixReply := c.messageToPrefix(message, previewName, fileName, false) reply = prefixReply replyObtained = true newText.WriteString(prefix) if text != "" { - // \n if it is groupchat and message is not empty if prefix != "" { - if chatId < 0 { - newText.WriteString("\n") - } else if chatId > 0 { - newText.WriteString(" | ") - } + newText.WriteString(c.getPrefixSeparator(chatId)) } - newText.WriteString(text) } text = newText.String() diff --git a/telegram/utils_test.go b/telegram/utils_test.go index fa9c107..005d17b 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -436,7 +436,7 @@ func TestMessageToPrefix1(t *testing.T) { }, }, } - prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "") + prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "", "", false) if prefix != "➡ 42 | fwd: ziz" { t.Errorf("Wrong prefix: %v", prefix) } @@ -454,7 +454,7 @@ func TestMessageToPrefix2(t *testing.T) { }, }, } - prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "") + prefix, gatewayReply := (&Client{Session: &persistence.Session{}}).messageToPrefix(&message, "y.jpg", "", false) if prefix != "⬅ 56 | fwd: (zaz) | preview: y.jpg" { t.Errorf("Wrong prefix: %v", prefix) } @@ -472,7 +472,7 @@ func TestMessageToPrefix3(t *testing.T) { }, }, } - prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg") + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "a.jpg", false) if prefix != "< 56 | fwd: (zuz) | file: a.jpg" { t.Errorf("Wrong prefix: %v", prefix) } @@ -486,7 +486,7 @@ func TestMessageToPrefix4(t *testing.T) { Id: 23, IsOutgoing: true, } - prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false) if prefix != "> 23" { t.Errorf("Wrong prefix: %v", prefix) } @@ -504,7 +504,7 @@ func TestMessageToPrefix5(t *testing.T) { }, }, } - prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg") + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "h.jpg", "a.jpg", false) if prefix != "< 560 | fwd: (zyz) | preview: h.jpg | file: a.jpg" { t.Errorf("Wrong prefix: %v", prefix) } @@ -530,7 +530,7 @@ func TestMessageToPrefix6(t *testing.T) { }, }, } - prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false) if prefix != "> 23 | reply: ziz @ unknown contact: TDlib instance is offline | tist uz iz" { t.Errorf("Wrong prefix: %v", prefix) } @@ -556,7 +556,7 @@ func TestMessageToPrefix7(t *testing.T) { }, }, } - prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "") + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", false) if prefix != "> 23 | reply: (zaz) @ unknown contact: TDlib instance is offline | tist" { t.Errorf("Wrong prefix: %v", prefix) } @@ -565,6 +565,32 @@ func TestMessageToPrefix7(t *testing.T) { } } +func TestMessageToPrefix8(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: "zuz", + }, + }, + } + prefix, gatewayReply := (&Client{Session: &persistence.Session{AsciiArrows: true}}).messageToPrefix(&message, "", "", true) + if prefix != "> 23" { + t.Errorf("Wrong prefix: %v", prefix) + } + if gatewayReply != nil { + t.Errorf("Reply is not nil: %v", gatewayReply) + } +} + func GetSenderIdEmpty(t *testing.T) { message := client.Message{} senderId := (&Client{}).getSenderId(&message) From a74e2bcb7d3262073d05aa89140b1d202b7f179d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 5 May 2024 13:16:38 -0400 Subject: [PATCH 39/54] Mute/unmute whole chats with no arguments --- Makefile | 2 +- persistence/sessions.go | 100 ++++++++++++++++++++++++++++++---- persistence/sessions_test.go | 28 ++++++++++ telegabber.go | 2 +- telegram/commands.go | 102 ++++++++++++++++++----------------- telegram/handlers.go | 11 ++++ 6 files changed, 184 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index a90eb8f..f8d5b73 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.3" +VERSION := "v1.9.4" MAKEOPTS := "-j4" all: diff --git a/persistence/sessions.go b/persistence/sessions.go index 29c4918..0454d97 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -3,6 +3,7 @@ package persistence import ( "github.com/pkg/errors" "io/ioutil" + "sync" "time" "dev.narayana.im/narayana/telegabber/yamldb" @@ -34,16 +35,18 @@ type SessionsMap struct { // Session is a key-values subtree type Session struct { - Login string `yaml:":login"` - Timezone string `yaml:":timezone"` - KeepOnline bool `yaml:":keeponline"` - RawMessages bool `yaml:":rawmessages"` - AsciiArrows bool `yaml:":asciiarrows"` - OOBMode bool `yaml:":oobmode"` - Carbons bool `yaml:":carbons"` - HideIds bool `yaml:":hideids"` - Receipts bool `yaml:":receipts"` - NativeEdits bool `yaml:":nativeedits"` + Login string `yaml:":login"` + Timezone string `yaml:":timezone"` + KeepOnline bool `yaml:":keeponline"` + RawMessages bool `yaml:":rawmessages"` + AsciiArrows bool `yaml:":asciiarrows"` + OOBMode bool `yaml:":oobmode"` + Carbons bool `yaml:":carbons"` + HideIds bool `yaml:":hideids"` + Receipts bool `yaml:":receipts"` + NativeEdits bool `yaml:":nativeedits"` + IgnoredChats []int64 `yaml:":ignoredchats"` + ignoredChatsMap map[int64]bool `yaml:"-"` } var configKeys = []string{ @@ -59,14 +62,21 @@ var configKeys = []string{ } var sessionDB *SessionsYamlDB +var sessionsLock sync.Mutex // SessionMarshaller implementation for YamlDB func SessionMarshaller() ([]byte, error) { cleanedMap := SessionsMap{} emptySessionsMap(&cleanedMap) + sessionsLock.Lock() + defer sessionsLock.Unlock() for jid, session := range sessionDB.Data.Sessions { if session.Login != "" { + session.IgnoredChats = make([]int64, 0, len(session.ignoredChatsMap)) + for chatID := range session.ignoredChatsMap { + session.IgnoredChats = append(session.IgnoredChats, chatID) + } cleanedMap.Sessions[jid] = session } } @@ -108,6 +118,16 @@ func initYamlDB(path string, dataPtr *SessionsMap) (*SessionsYamlDB, error) { emptySessionsMap(dataPtr) } + // convert ignored users slice to map + for jid, session := range dataPtr.Sessions { + session.ignoredChatsMap = make(map[int64]bool) + for _, chatID := range session.IgnoredChats { + session.ignoredChatsMap[chatID] = true + } + session.IgnoredChats = nil + dataPtr.Sessions[jid] = session + } + return &SessionsYamlDB{ YamlDB: yamldb.YamlDB{ Path: path, @@ -119,6 +139,13 @@ func initYamlDB(path string, dataPtr *SessionsMap) (*SessionsYamlDB, error) { // Get retrieves a session value func (s *Session) Get(key string) (string, error) { + sessionsLock.Lock() + defer sessionsLock.Unlock() + + return s.get(key) +} + +func (s *Session) get(key string) (string, error) { switch key { case "timezone": return s.Timezone, nil @@ -145,9 +172,12 @@ func (s *Session) Get(key string) (string, error) { // ToMap converts the session to a map func (s *Session) ToMap() map[string]string { + sessionsLock.Lock() + defer sessionsLock.Unlock() + m := make(map[string]string) for _, configKey := range configKeys { - value, _ := s.Get(configKey) + value, _ := s.get(configKey) m[configKey] = value } @@ -156,6 +186,9 @@ func (s *Session) ToMap() map[string]string { // Set sets a session value func (s *Session) Set(key string, value string) (string, error) { + sessionsLock.Lock() + defer sessionsLock.Unlock() + switch key { case "timezone": s.Timezone = value @@ -232,6 +265,51 @@ func (s *Session) TimezoneToLocation() *time.Location { return zeroLocation } +// IgnoreChat adds a chat id to ignore list, returns false if already ignored +func (s *Session) IgnoreChat(chatID int64) bool { + sessionsLock.Lock() + defer sessionsLock.Unlock() + + if s.ignoredChatsMap == nil { + s.ignoredChatsMap = make(map[int64]bool) + } else if _, ok := s.ignoredChatsMap[chatID]; ok { + return false + } + + s.ignoredChatsMap[chatID] = true + return true +} + +// UnignoreChat removes a chat id from ignore list, returns false if not already ignored +func (s *Session) UnignoreChat(chatID int64) bool { + sessionsLock.Lock() + defer sessionsLock.Unlock() + + if s.ignoredChatsMap == nil { + return false + } + + if _, ok := s.ignoredChatsMap[chatID]; !ok { + return false + } + + delete(s.ignoredChatsMap, chatID) + return true +} + +// IsChatIgnored checks the chat id against the ignore list +func (s *Session) IsChatIgnored(chatID int64) bool { + sessionsLock.Lock() + defer sessionsLock.Unlock() + + if s.ignoredChatsMap == nil { + return false + } + + _, ok := s.ignoredChatsMap[chatID] + return ok +} + func fromBool(b bool) string { if b { return "true" diff --git a/persistence/sessions_test.go b/persistence/sessions_test.go index 0339378..001cfa0 100644 --- a/persistence/sessions_test.go +++ b/persistence/sessions_test.go @@ -88,3 +88,31 @@ func TestSessionSetAbsent(t *testing.T) { t.Error("There shouldn't come a donkey!") } } + +func TestSessionIgnore(t *testing.T) { + session := Session{} + if session.IsChatIgnored(3) { + t.Error("Shouldn't be ignored yet") + } + if !session.IgnoreChat(3) { + t.Error("Shouldn't have been ignored") + } + if session.IgnoreChat(3) { + t.Error("Shouldn't ignore second time") + } + if !session.IsChatIgnored(3) { + t.Error("Should be ignored already") + } + if session.IsChatIgnored(-145) { + t.Error("Wrong chat is ignored") + } + if !session.UnignoreChat(3) { + t.Error("Should successfully unignore") + } + if session.UnignoreChat(3) { + t.Error("Should unignore second time") + } + if session.IsChatIgnored(3) { + t.Error("Shouldn't be ignored already") + } +} diff --git a/telegabber.go b/telegabber.go index c39d91d..9e71887 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.3" +var version string = "1.9.4" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 1ce316b..4730a3f 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -85,8 +85,8 @@ var chatCommands = map[string]command{ "invite": command{"id or @username", "add user to current chat"}, "link": command{"", "get invite link for current chat"}, "kick": command{"id or @username", "remove user to current chat"}, - "mute": command{"id or @username [hours]", "mute user in current chat"}, - "unmute": command{"id or @username", "unrestrict user from current chat"}, + "mute": command{"[id or @username] [hours]", "mute the whole chat or a user in current chat"}, + "unmute": command{"[id or @username]", "unmute the whole chat or a user in the current chat"}, "ban": command{"id or @username [hours]", "restrict @username from current chat for [hours] or forever"}, "unban": command{"id or @username", "unbans @username in current chat (and devotes from admins)"}, "promote": command{"id or @username [title]", "promote user to admin in current chat"}, @@ -771,59 +771,65 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) if err != nil { return err.Error(), true } - // mute @username [n hours] + // mute [@username [n hours]] case "mute": - if len(args) < 1 { - return notEnoughArguments, true - } - - contact, _, err := c.GetContactByUsername(args[0]) - if err != nil { - return err.Error(), true - } - - var hours int64 - if len(args) > 1 { - hours, err = strconv.ParseInt(args[1], 10, 32) + if len(args) > 0 { + contact, _, err := c.GetContactByUsername(args[0]) if err != nil { - return "Invalid number of hours", true + return err.Error(), true } - } - _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ - ChatId: chatID, - MemberId: &client.MessageSenderUser{UserId: contact.Id}, - Status: &client.ChatMemberStatusRestricted{ - IsMember: true, - RestrictedUntilDate: c.formatBantime(hours), - Permissions: &permissionsReadonly, - }, - }) - if err != nil { - return err.Error(), true + var hours int64 + if len(args) > 1 { + hours, err = strconv.ParseInt(args[1], 10, 32) + if err != nil { + return "Invalid number of hours", true + } + } + + _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ + ChatId: chatID, + MemberId: &client.MessageSenderUser{UserId: contact.Id}, + Status: &client.ChatMemberStatusRestricted{ + IsMember: true, + RestrictedUntilDate: c.formatBantime(hours), + Permissions: &permissionsReadonly, + }, + }) + if err != nil { + return err.Error(), true + } + } else { + if !c.Session.IgnoreChat(chatID) { + return "Chat is already ignored", true + } + gateway.DirtySessions = true } - // unmute @username + // unmute [@username] case "unmute": - if len(args) < 1 { - return notEnoughArguments, true - } + if len(args) > 0 { + contact, _, err := c.GetContactByUsername(args[0]) + if err != nil { + return err.Error(), true + } - contact, _, err := c.GetContactByUsername(args[0]) - if err != nil { - return err.Error(), true - } - - _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ - ChatId: chatID, - MemberId: &client.MessageSenderUser{UserId: contact.Id}, - Status: &client.ChatMemberStatusRestricted{ - IsMember: true, - RestrictedUntilDate: 0, - Permissions: &permissionsMember, - }, - }) - if err != nil { - return err.Error(), true + _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ + ChatId: chatID, + MemberId: &client.MessageSenderUser{UserId: contact.Id}, + Status: &client.ChatMemberStatusRestricted{ + IsMember: true, + RestrictedUntilDate: 0, + Permissions: &permissionsMember, + }, + }) + if err != nil { + return err.Error(), true + } + } else { + if !c.Session.UnignoreChat(chatID) { + return "Chat wasn't ignored", true + } + gateway.DirtySessions = true } // ban @username from current chat [for N hours] case "ban": diff --git a/telegram/handlers.go b/telegram/handlers.go index 6266292..1ccd622 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -235,6 +235,9 @@ func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) { // message received func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { chatId := update.Message.ChatId + if c.Session.IsChatIgnored(chatId) { + return + } // guarantee sequential message delivering per chat lock := c.getChatMessageLock(chatId) @@ -261,6 +264,10 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { // message content updated func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { + if c.Session.IsChatIgnored(update.ChatId) { + return + } + markupFunction := c.getFormatter() defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent) @@ -353,6 +360,10 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { // message(s) deleted func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { if update.IsPermanent { + if c.Session.IsChatIgnored(update.ChatId) { + return + } + var deleteChar string if c.Session.AsciiArrows { deleteChar = "X " From af07773b07ed3d0138ad237906bcd8e81512a11d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 10 May 2024 19:22:53 -0400 Subject: [PATCH 40/54] Random IDs for service messages --- Makefile | 2 +- telegabber.go | 2 +- xmpp/gateway/gateway.go | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f8d5b73..4452163 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.4" +VERSION := "v1.9.5" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 9e71887..6cfccff 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.4" +var version string = "1.9.5" var commit string var sm *goxmpp.StreamManager diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index de0ec8d..1507e31 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -10,6 +10,7 @@ import ( "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/xmpp/extensions" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/soheilhy/args" "gosrc.io/xmpp" @@ -61,12 +62,20 @@ func SendMessage(to string, from string, body string, id string, component *xmpp // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to string, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", component, nil, nil, "", "", false, false) + var id string + if uuid, err := uuid.NewRandom(); err == nil { + id = uuid.String() + } + sendMessageWrapper(to, "", body, id, component, nil, nil, "", "", false, false) } // SendTextMessage creates and sends a simple message stanza func SendTextMessage(to string, from string, body string, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", component, nil, nil, "", "", false, false) + var id string + if uuid, err := uuid.NewRandom(); err == nil { + id = uuid.String() + } + sendMessageWrapper(to, from, body, id, component, nil, nil, "", "", false, false) } // SendMessageWithOOB creates and sends a message stanza with OOB URL From 249c942fc2d9f017ffb66c98f22e7f2a2c40ad2a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 10 May 2024 19:53:16 -0400 Subject: [PATCH 41/54] Allow empty form for mute/unmute commands --- xmpp/handlers.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1d77bc4..1a47c10 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -808,6 +808,13 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command members, err := session.GetChatMembers(toId, true, "", membersList) if err == nil { fieldType = stanza.FieldTypeListSingle + switch command.Node { + // allow empty form + case "mute", "unmute": + options = append(options, stanza.Option{ + ValuesList: []string{""}, + }) + } for _, member := range members { senderId := session.GetSenderId(member.MemberId) options = append(options, stanza.Option{ From e94a646e19b3bca5be4e97b6d756d257034a5788 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 12 May 2024 11:03:48 -0400 Subject: [PATCH 42/54] Upgrade to go-xmpp version with multiple command elements support --- go.mod | 5 +++-- go.sum | 2 ++ xmpp/handlers.go | 33 +++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index fe7aeb4..4eb2643 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/dgraph-io/badger/v4 v4.1.0 + github.com/google/uuid v1.1.1 github.com/pkg/errors v0.9.1 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.4.2 @@ -23,7 +24,6 @@ require ( github.com/golang/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.3 // indirect github.com/google/flatbuffers v1.12.1 // indirect - github.com/google/uuid v1.1.1 // indirect github.com/klauspost/compress v1.12.3 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect go.opencensus.io v0.22.5 // indirect @@ -33,5 +33,6 @@ require ( nhooyr.io/websocket v1.6.5 // indirect ) -replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd +replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314 + replace github.com/zelenin/go-tdlib => dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 diff --git a/go.sum b/go.sum index f5e218f..82e391a 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMj dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd h1:+UW+E7JjI88aH4beDn1cw6D8rs1I061hN91HU4Y4pT8= dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314 h1:29/NjOGOUDceO73Hk4Nj4uVa1je8MULJlsDSvKxSN/k= +dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1a47c10..2615290 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -749,13 +749,20 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command var cmdString string var cmdType telegram.CommandType - form, formOk := command.CommandElement.(*stanza.Form) + var form *stanza.Form + for _, ce := range command.CommandElements { + fo, formOk := ce.(*stanza.Form) + if formOk { + form = fo + break + } + } if toOk { cmdType = telegram.CommandTypeChat } else { cmdType = telegram.CommandTypeTransport } - if formOk { + if form != nil { // just for the case the client messed the order somehow sort.Slice(form.Fields, func(i int, j int) bool { iField := form.Fields[i] @@ -844,10 +851,10 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command Fields: fields, } answer.Payload = &stanza.Command{ - SessionId: command.Node, - Node: command.Node, - Status: stanza.CommandStatusExecuting, - CommandElement: &form, + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusExecuting, + CommandElements: []stanza.CommandElement{&form}, } log.Debugf("form: %#v", form) } else { @@ -884,12 +891,14 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command } answer.Payload = &stanza.Command{ - SessionId: command.Node, - Node: command.Node, - Status: stanza.CommandStatusCompleted, - CommandElement: &stanza.Note{ - Text: response, - Type: noteType, + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCompleted, + CommandElements: []stanza.CommandElement{ + &stanza.Note{ + Text: response, + Type: noteType, + }, }, } From bd5f41a76ba38ae5f51bbcfc89ec086984cc9e9a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 12 May 2024 11:05:18 -0400 Subject: [PATCH 43/54] Fix missing go.sum entry errors in staging.Dockerfile --- staging.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/staging.Dockerfile b/staging.Dockerfile index e9fdd1e..95cdea5 100644 --- a/staging.Dockerfile +++ b/staging.Dockerfile @@ -26,8 +26,8 @@ WORKDIR /src RUN go env -w GOCACHE=/go-cache RUN go env -w GOMODCACHE=/gomod-cache RUN --mount=type=cache,target=/gomod-cache \ - --mount=type=bind,source=./,target=/src \ - go mod download + --mount=type=bind,source=./,target=/src,rw \ + /bin/bash -c 'go mod tidy; go get -t' FROM cache AS build ARG MAKEOPTS From ba8f4c08cf70c062a9f40147c6245d348e8f8346 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 1 Jun 2024 16:43:38 -0400 Subject: [PATCH 44/54] Attach prefix to OOB descriptions and omit empty ones only if sender is displayed by carbon --- Makefile | 2 +- telegabber.go | 2 +- telegram/utils.go | 25 +++++++++++++++++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 4452163..fccaf92 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.5" +VERSION := "v1.9.6" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 6cfccff..f315de5 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.5" +var version string = "1.9.6" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index 4509d1a..819455c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1058,13 +1058,19 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { fileName, link := c.formatFile(file, false) oob = link - if c.Session.OOBMode && oob != "" { - typ := message.Content.MessageContentType() - if typ != client.TypeMessageSticker { - auxText = text + oobSwap := c.Session.OOBMode && oob != "" + + var ignorePrefix bool + if oobSwap { + if text == "" || message.Content.MessageContentType() == client.TypeMessageSticker { + isPM, err := c.IsPM(chatId) + if err == nil { + ignorePrefix = isPM && c.isCarbonsEnabled() + } } - text = oob - } else if !c.Session.RawMessages { + } + + if !c.Session.RawMessages && !ignorePrefix { var newText strings.Builder prefix, prefixReply := c.messageToPrefix(message, previewName, fileName, false) @@ -1080,6 +1086,13 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } text = newText.String() } + + if oobSwap { + if !ignorePrefix { + auxText = text + } + text = oob + } } } if !replyObtained { From 85485bb1473337c63e15353e3ddd6a0f3effc9d0 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 12 Jan 2025 22:05:24 -0500 Subject: [PATCH 45/54] Retrieve XMPP client features --- telegram/client.go | 32 ++++++++++++++----------- xmpp/handlers.go | 59 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 79f27d5..846e5a3 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -48,6 +48,9 @@ type Client struct { lastMsgIds map[int64]string msgHashSeed maphash.Seed + XmppClientFeatures map[string]*[]string + XmppClientFeaturesLock sync.Mutex + locks clientLocks SendMessageLock sync.Mutex } @@ -109,20 +112,21 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component } return &Client{ - parameters: ¶meters, - xmpp: component, - jid: jid, - Session: session, - resources: make(map[string]bool), - content: &conf.Content, - cache: cache.NewCache(), - outbox: make(map[string]string), - editOutbox: make(map[string]string), - options: options, - DelayedStatuses: make(map[int64]*DelayedStatus), - lastMsgHashes: make(map[int64]uint64), - lastMsgIds: make(map[int64]string), - msgHashSeed: maphash.MakeSeed(), + parameters: ¶meters, + xmpp: component, + jid: jid, + Session: session, + resources: make(map[string]bool), + content: &conf.Content, + cache: cache.NewCache(), + outbox: make(map[string]string), + editOutbox: make(map[string]string), + options: options, + DelayedStatuses: make(map[int64]*DelayedStatus), + lastMsgHashes: make(map[int64]uint64), + lastMsgIds: make(map[int64]string), + msgHashSeed: maphash.MakeSeed(), + XmppClientFeatures: make(map[string]*[]string), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), }, diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 811cef6..d5ce6f0 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -15,6 +15,7 @@ import ( "dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/gateway" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/soheilhy/args" "gosrc.io/xmpp" @@ -40,7 +41,7 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { } log.Debugf("%#v", iq) - if iq.Type == "get" { + if iq.Type == stanza.IQTypeGet { _, ok := iq.Payload.(*extensions.IqVcardTemp) if ok { go handleGetVcardIq(s, iq, TypeVCardTemp) @@ -68,12 +69,18 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetQueryRegister(s, iq) return } - } else if iq.Type == "set" { + } else if iq.Type == stanza.IQTypeSet { query, ok := iq.Payload.(*extensions.QueryRegister) if ok { go handleSetQueryRegister(s, iq, query) return } + } else if iq.Type == stanza.IQTypeResult { + discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) + if ok { + go handleClientFeatures(iq, discoInfo) + return + } } } @@ -412,6 +419,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { newArgs..., ) } + probeClientFeatures(p.From, component) session.UpdateChatNicknames() } }() @@ -687,6 +695,53 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code } } +func probeClientFeatures(jid string, component *xmpp.Component) { + id, err := uuid.NewRandom() + if err != nil { + log.Error("Could not generate ID for a client features probe") + return + } + + probe := stanza.IQ{ + Attrs: stanza.Attrs{ + From: gateway.Jid.Bare(), + To: jid, + Id: id.String(), + Type: stanza.IQTypeGet, + }, + Payload: &stanza.DiscoInfo{}, + } + log.Debugf("%#v", probe) + + gateway.ResumableSend(component, &probe) +} + +func handleClientFeatures(iq *stanza.IQ, discoInfo *stanza.DiscoInfo) { + fromJid, err := stanza.NewJid(iq.From) + if err != nil { + log.Error("Invalid from JID!") + return + } + bareFrom := fromJid.Bare() + + session, ok := sessions[bareFrom] + if !ok { + log.Errorf("Got client features for unknown JID %v", bareFrom) + return + } + + var features []string + for _, feature := range discoInfo.Features { + features = append(features, feature.Var) + } + + session.XmppClientFeaturesLock.Lock() + session.XmppClientFeatures[fromJid.Resource] = &features + session.XmppClientFeaturesLock.Unlock() + + log.Debugf("Features for %v: %#v", iq.From, features) +} + func toToID(to string) (int64, bool) { toParts := strings.Split(to, "@") if len(toParts) < 2 { From 421477ad8c0df4b1c5a9adacfa1aea93efaf2419 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 14 Jan 2025 13:10:57 -0500 Subject: [PATCH 46/54] Support avatar notifications and retrieval via XEP-0084 --- telegram/client.go | 10 +++ telegram/utils.go | 131 ++++++++++++++++++++++++++---- xmpp/gateway/gateway.go | 46 +++++++++++ xmpp/handlers.go | 176 ++++++++++++++++++++++++++++++++++------ 4 files changed, 322 insertions(+), 41 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 846e5a3..385ccd3 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -22,6 +22,12 @@ type DelayedStatus struct { TimestampExpired int64 } +// HashedAvatar stores a SHA-1 hash and a Telegram file ID +type HashedAvatar struct { + Hash string + File int32 +} + // Client stores the metadata for lazily invoked TDlib instance type Client struct { client *client.Client @@ -51,6 +57,9 @@ type Client struct { XmppClientFeatures map[string]*[]string XmppClientFeaturesLock sync.Mutex + AvatarHashes map[int64]*HashedAvatar + AvatarHashesLock sync.Mutex + locks clientLocks SendMessageLock sync.Mutex } @@ -127,6 +136,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component lastMsgIds: make(map[int64]string), msgHashSeed: maphash.MakeSeed(), XmppClientFeatures: make(map[string]*[]string), + AvatarHashes: make(map[int64]*HashedAvatar), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), }, diff --git a/telegram/utils.go b/telegram/utils.go index 819455c..cbdbe85 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1,7 +1,9 @@ package telegram import ( + "bytes" "crypto/sha1" + "encoding/base64" "encoding/binary" "fmt" "github.com/pkg/errors" @@ -45,6 +47,11 @@ type messageStub struct { Text string } +const ( + typeFileDataSha1 byte = iota + typeFileDataBase64 +) + var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) @@ -211,6 +218,85 @@ func (c *Client) LastSeenStatus(timestamp int64) string { Format("Last seen at 15:04 02/01/2006") } +func (c *Client) getFileData(tgFile *client.File, typ byte) string { + var priority int32 + if typ == typeFileDataSha1 { + priority = 1 + } else if typ == typeFileDataBase64 { + priority = 32 + } + + file, path, err := c.ForceOpenFile(tgFile, priority) + if err == nil { + defer file.Close() + + if typ == typeFileDataSha1 { + hash := sha1.New() + _, err = io.Copy(hash, file) + if err == nil { + return fmt.Sprintf("%x", hash.Sum(nil)) + } else { + log.Errorf("Error calculating hash: %v", path) + } + } else if typ == typeFileDataBase64 { + buf := new(bytes.Buffer) + binval := base64.NewEncoder(base64.StdEncoding, buf) + _, err = io.Copy(binval, file) + binval.Close() + if err == nil { + return buf.String() + } else { + log.Errorf("Error calculating base64: %v", path) + } + } + } else if path != "" { + log.Errorf("Photo does not exist: %v", path) + } else { + log.Errorf("PHOTO: %#v", err.Error()) + } + + return "" +} + +// SetEmptyAvatarHash puts a dummy value into the cache to avoid attempting to fetch surely missing avatars +func (c *Client) SetEmptyAvatarHash(chatId int64) { + c.AvatarHashesLock.Lock() + c.AvatarHashes[chatId] = &HashedAvatar{ + Hash: "", + File: 0, + } + c.AvatarHashesLock.Unlock() +} + +// GetPhotoSha1AndSize obtains data for PEP +func (c *Client) GetPhotoSha1AndSize(photo *client.File, chatId int64) (string, int64) { + sha1 := c.GetPhotoSha1(photo, chatId) + + size := photo.Size + if size == 0 { + size = photo.ExpectedSize + } + + return sha1, size +} + +// GetPhotoSha1 computes the photo hash +func (c *Client) GetPhotoSha1(photo *client.File, chatId int64) string { + sha1 := c.getFileData(photo, typeFileDataSha1) + c.AvatarHashesLock.Lock() + c.AvatarHashes[chatId] = &HashedAvatar{ + Hash: sha1, + File: photo.Id, + } + c.AvatarHashesLock.Unlock() + return sha1 +} + +// GetPhotoBase64 reads file data as Base64 +func (c *Client) GetPhotoBase64(photo *client.File) string { + return c.getFileData(photo, typeFileDataBase64) +} + // ProcessStatusUpdate sets contact status func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, oldArgs ...args.V) error { if !c.Online() { @@ -228,20 +314,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o var photo string if chat != nil && chat.Photo != nil { - file, path, err := c.ForceOpenFile(chat.Photo.Small, 1) - if err == nil { - defer file.Close() - - hash := sha1.New() - _, err = io.Copy(hash, file) - if err == nil { - photo = fmt.Sprintf("%x", hash.Sum(nil)) - } else { - log.Errorf("Error calculating hash: %v", path) - } - } else if path != "" { - log.Errorf("Photo does not exist: %v", path) - } + photo = c.GetPhotoSha1(chat.Photo.Small, chatID) } var presenceType string @@ -1042,6 +1115,24 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { c.cache.SetChat(chatId, chat) go c.ProcessStatusUpdate(chatId, "", "", gateway.SPImmed(true)) text = "" + + if chat.Photo == nil { + c.SetEmptyAvatarHash(chatId) + } else { + sha1, size := c.GetPhotoSha1AndSize(chat.Photo.Small, chatId) + + for resource := range c.resourcesRange() { + features, ok := c.XmppClientFeatures[resource] + if ok && features != nil { + for _, feature := range *features { + if feature == gateway.NodeAvatarMetadataNotify { + go gateway.SendPubSubAvatarNotification(c.xmpp, c.jid+"/"+resource, chatId, sha1, size) + break + } + } + } + } + } } } else { text = c.messageToText(message, false) @@ -1263,6 +1354,11 @@ func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFi return content } +// ChatsKeys proxies the following function from unexported cache +func (c *Client) ChatsKeys() []int64 { + return c.cache.ChatsKeys() +} + // StatusesRange proxies the following function from unexported cache func (c *Client) StatusesRange() chan *cache.Status { return c.cache.StatusesRange() @@ -1337,6 +1433,13 @@ func (c *Client) getLastMessages(id int64, query string, from int64, count int32 }) } +// GetFile retrieves a file object by id given by TDlib +func (c *Client) GetFile(id int32) (*client.File, error) { + return c.client.GetFile(&client.GetFileRequest{ + FileId: id, + }) +} + // DownloadFile actually obtains a file by id given by TDlib func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*client.File, error) { return c.client.DownloadFile(&client.DownloadFileRequest{ diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 1507e31..685e800 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -37,6 +37,10 @@ type marker struct { } const NSNick string = "http://jabber.org/protocol/nick" +const NodeVCard4 string = "urn:xmpp:vcard4" +const NodeAvatarMetadata string = "urn:xmpp:avatar:metadata" +const NodeAvatarMetadataNotify string = NodeAvatarMetadata + "+notify" +const NodeAvatarData string = "urn:xmpp:avatar:data" // Queue stores presences to send later var Queue = make(map[string]*stanza.Presence) @@ -435,3 +439,45 @@ func SplitJID(from string) (string, string, bool) { } return fromJid.Bare(), fromJid.Resource, true } + +// SendPubSubAvatarNotification encourages clients to fetch an avatar +func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId int64, sha1 string, size int64) { + info := stanza.Node{ + XMLName: xml.Name{Local: "info"}, + Attrs: []xml.Attr{ + xml.Attr{Name: xml.Name{Local: "bytes"}, Value: strconv.FormatInt(size, 10)}, + xml.Attr{Name: xml.Name{Local: "height"}, Value: "160"}, + xml.Attr{Name: xml.Name{Local: "id"}, Value: sha1}, + xml.Attr{Name: xml.Name{Local: "type"}, Value: "image/jpeg"}, + xml.Attr{Name: xml.Name{Local: "width"}, Value: "160"}, + }, + } + log.WithFields(log.Fields{ + "chatId": chatId, + }).Debugf("%#v", info) + + event := &stanza.PubSubEvent{ + EventElement: &stanza.ItemsEvent{ + Node: NodeAvatarMetadata, + Items: []stanza.ItemEvent{ + stanza.ItemEvent{ + Id: sha1, + Any: &stanza.Node{ + XMLName: xml.Name{Local: "metadata", Space: NodeAvatarMetadata}, + Nodes: []stanza.Node{info}, + }, + }, + }, + }, + } + + message := stanza.Message{ + Attrs: stanza.Attrs{ + From: strconv.FormatInt(chatId, 10) + "@" + Jid.Bare(), + To: jid, + }, + Extensions: []stanza.MsgExtension{event}, + } + + _ = ResumableSend(component, message) +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index d5ce6f0..40f90b5 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1,12 +1,9 @@ package xmpp import ( - "bytes" - "encoding/base64" "encoding/xml" "fmt" "github.com/pkg/errors" - "io" "strconv" "strings" @@ -26,7 +23,6 @@ const ( TypeVCardTemp byte = iota TypeVCard4 ) -const NodeVCard4 string = "urn:xmpp:vcard4" func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) @@ -48,11 +44,15 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { return } pubsub, ok := iq.Payload.(*stanza.PubSubGeneric) - if ok { - if pubsub.Items != nil && pubsub.Items.Node == NodeVCard4 { + if ok && pubsub.Items != nil { + if pubsub.Items.Node == gateway.NodeVCard4 { go handleGetVcardIq(s, iq, TypeVCard4) return } + if pubsub.Items.Node == gateway.NodeAvatarData { + go handleGetAvatarDataIq(s, iq, pubsub) + return + } } _, ok = iq.Payload.(*stanza.DiscoInfo) if ok { @@ -78,7 +78,7 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { } else if iq.Type == stanza.IQTypeResult { discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { - go handleClientFeatures(iq, discoInfo) + go handleClientFeatures(s, iq, discoInfo) return } } @@ -476,6 +476,113 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { _ = gateway.ResumableSend(component, &answer) } +func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGeneric) { + fromJid, err := stanza.NewJid(iq.From) + if err != nil { + log.Errorf("Invalid from JID %v", iq.From) + return + } + + chatId, ok := toToID(iq.To) + if !ok { + log.Errorf("Invalid chat id in To JID %v", iq.To) + return + } + + session, ok := sessions[fromJid.Bare()] + if !ok { + log.Errorf("IQ from stranger %v", iq.From) + return + } + + var id string + if len(pubsub.Items.List) > 0 { + id = pubsub.Items.List[0].Id + } + log.Infof("Avatar id %v for chat %v", id, iq.To); + + pubsubAnswer := stanza.PubSubGeneric{ + Items: &stanza.Items{ + Node: gateway.NodeAvatarData, + }, + } + + answer := stanza.IQ{ + Attrs: stanza.Attrs{ + From: iq.To, + To: iq.From, + Id: iq.Id, + Type: "result", + }, + Payload: &pubsubAnswer, + } + + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + defer gateway.ResumableSend(component, &answer) + + hashedAvatar, ok := session.AvatarHashes[chatId] + if !ok { + log.Info("Could not find avatar in cache, fetching immediately") + + chat, _, err := session.GetContactByID(chatId, nil) + if err != nil || chat == nil || chat.Photo == nil { + return + } + + file := chat.Photo.Small + + sha1 := session.GetPhotoSha1(file, chatId) + hashedAvatar = &telegram.HashedAvatar{ + Hash: sha1, + File: file.Id, + } + + session.AvatarHashesLock.Lock() + session.AvatarHashes[chatId] = hashedAvatar + session.AvatarHashesLock.Unlock() + } + + if id != "" && hashedAvatar.Hash != id { + log.Infof("Cache contains %v hash for chat %v, but %v was requested; aborting", hashedAvatar.Hash, iq.To, id) + return + } + if hashedAvatar.File == 0 { + log.Infof("Avatar for chat %v is explicitly missing", iq.To) + return + } + + file, err := session.GetFile(hashedAvatar.File) + if err != nil { + log.WithFields(log.Fields{ + "chatId": chatId, + }).Error(errors.Wrap(err, "Cannot get avatar file")) + return + } + + dataString := session.GetPhotoBase64(file) + if dataString == "" { + log.Errorf("Error reading avatar file for chat %v", iq.To) + return + } + + pubsubAnswer.Items.List = append(pubsubAnswer.Items.List, stanza.Item{ + Id: hashedAvatar.Hash, + Any: &stanza.Node{ + XMLName: xml.Name{Local: "data", Space: gateway.NodeAvatarData}, + Content: dataString, + }, + }) + + log.WithFields(log.Fields{ + "length": len(dataString), + }).Debugf("%#v", answer) +} + func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, @@ -716,7 +823,7 @@ func probeClientFeatures(jid string, component *xmpp.Component) { gateway.ResumableSend(component, &probe) } -func handleClientFeatures(iq *stanza.IQ, discoInfo *stanza.DiscoInfo) { +func handleClientFeatures(s xmpp.Sender, iq *stanza.IQ, discoInfo *stanza.DiscoInfo) { fromJid, err := stanza.NewJid(iq.From) if err != nil { log.Error("Invalid from JID!") @@ -731,8 +838,12 @@ func handleClientFeatures(iq *stanza.IQ, discoInfo *stanza.DiscoInfo) { } var features []string + var avatarNotify bool for _, feature := range discoInfo.Features { features = append(features, feature.Var) + if feature.Var == gateway.NodeAvatarMetadataNotify { + avatarNotify = true + } } session.XmppClientFeaturesLock.Lock() @@ -740,6 +851,34 @@ func handleClientFeatures(iq *stanza.IQ, discoInfo *stanza.DiscoInfo) { session.XmppClientFeaturesLock.Unlock() log.Debugf("Features for %v: %#v", iq.From, features) + + if avatarNotify { + go sendPubSubAvatarNotifications(s, iq.From, session) + } +} + +func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram.Client) { + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + for _, chatId := range session.ChatsKeys() { + chat, _, err := session.GetContactByID(chatId, nil) + if err != nil || chat == nil { + continue + } + + if chat.Photo == nil { + session.SetEmptyAvatarHash(chatId) + continue + } + + sha1, size := session.GetPhotoSha1AndSize(chat.Photo.Small, chat.Id) + + gateway.SendPubSubAvatarNotification(component, jid, chat.Id, sha1, size) + } } func toToID(to string) (int64, bool) { @@ -760,24 +899,7 @@ func toToID(to string) (int64, bool) { func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload { var base64Photo string if info.Photo != nil { - file, path, err := session.ForceOpenFile(info.Photo, 32) - if err == nil { - defer file.Close() - - buf := new(bytes.Buffer) - binval := base64.NewEncoder(base64.StdEncoding, buf) - _, err = io.Copy(binval, file) - binval.Close() - if err == nil { - base64Photo = buf.String() - } else { - log.Errorf("Error calculating base64: %v", path) - } - } else if path != "" { - log.Errorf("Photo does not exist: %v", path) - } else { - log.Errorf("PHOTO: %#v", err.Error()) - } + base64Photo = session.GetPhotoBase64(info.Photo) } if typ == TypeVCardTemp { @@ -878,7 +1000,7 @@ func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *tel pubsub := &stanza.PubSubGeneric{ Items: &stanza.Items{ - Node: NodeVCard4, + Node: gateway.NodeVCard4, List: []stanza.Item{ stanza.Item{ Id: id, From 43399a1fbccfc6a54b52003b44fa10c407c45c79 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 16 Jan 2025 10:48:23 -0500 Subject: [PATCH 47/54] Fix PubSub avatar notifications for Monal --- Makefile | 2 +- telegabber.go | 2 +- xmpp/gateway/gateway.go | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fccaf92..a240b94 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.9.6" +VERSION := "v1.9.7" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index f315de5..5e4793b 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.9.6" +var version string = "1.9.7" var commit string var sm *goxmpp.StreamManager diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 685e800..88c9d57 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -475,6 +475,7 @@ func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId Attrs: stanza.Attrs{ From: strconv.FormatInt(chatId, 10) + "@" + Jid.Bare(), To: jid, + Type: stanza.MessageTypeHeadline, }, Extensions: []stanza.MsgExtension{event}, } From c5e41c7ce85a151bebf26e7a186cf3e3acc2bb3c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 22 Mar 2025 04:18:44 -0400 Subject: [PATCH 48/54] Version 1.10.0 --- Makefile | 2 +- telegabber.go | 2 +- telegram/utils.go | 2 +- xmpp/gateway/gateway.go | 2 +- xmpp/handlers.go | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 07d25e1..e3ad73e 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.10.0-dev" +VERSION := "v1.10.0" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index d39820d..732a6b7 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.10.0-dev" +var version string = "1.10.0" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index 4d75ad0..98d9d11 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -48,7 +48,7 @@ type messageStub struct { } const ( - typeFileDataSha1 byte = iota + typeFileDataSha1 byte = iota typeFileDataBase64 ) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 504d7db..0b6b492 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -465,7 +465,7 @@ func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId Id: sha1, Any: &stanza.Node{ XMLName: xml.Name{Local: "metadata", Space: NodeAvatarMetadata}, - Nodes: []stanza.Node{info}, + Nodes: []stanza.Node{info}, }, }, }, diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 2dc0277..aed08d9 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -505,7 +505,7 @@ func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGe if len(pubsub.Items.List) > 0 { id = pubsub.Items.List[0].Id } - log.Infof("Avatar id %v for chat %v", id, iq.To); + log.Infof("Avatar id %v for chat %v", id, iq.To) pubsubAnswer := stanza.PubSubGeneric{ Items: &stanza.Items{ @@ -1005,9 +1005,9 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command } answer.Payload = &stanza.Command{ - SessionId: command.Node, - Node: command.Node, - Status: stanza.CommandStatusCompleted, + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCompleted, CommandElements: []stanza.CommandElement{ &stanza.Note{ Text: response, From 7ebcdb08263de6a66886a042b811dbddc38d3674 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 22 Mar 2025 10:35:30 -0400 Subject: [PATCH 49/54] Add `ignoregroupdeletions` configuration option --- Makefile | 2 +- persistence/sessions.go | 12 ++++++++++++ telegabber.go | 2 +- telegram/handlers.go | 6 ++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e3ad73e..80f1aff 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.10.0" +VERSION := "v1.10.1" MAKEOPTS := "-j4" all: diff --git a/persistence/sessions.go b/persistence/sessions.go index 0454d97..a5f658a 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -47,6 +47,8 @@ type Session struct { NativeEdits bool `yaml:":nativeedits"` IgnoredChats []int64 `yaml:":ignoredchats"` ignoredChatsMap map[int64]bool `yaml:"-"` + + IgnoreGroupDeletions bool `yaml:":ignoregroupdeletions"` } var configKeys = []string{ @@ -59,6 +61,7 @@ var configKeys = []string{ "hideids", "receipts", "nativeedits", + "ignoregroupdeletions", } var sessionDB *SessionsYamlDB @@ -165,6 +168,8 @@ func (s *Session) get(key string) (string, error) { return fromBool(s.Receipts), nil case "nativeedits": return fromBool(s.NativeEdits), nil + case "ignoregroupdeletions": + return fromBool(s.IgnoreGroupDeletions), nil } return "", errors.New("Unknown session property") @@ -249,6 +254,13 @@ func (s *Session) Set(key string, value string) (string, error) { } s.NativeEdits = b return value, nil + case "ignoregroupdeletions": + b, err := toBool(value) + if err != nil { + return "", err + } + s.IgnoreGroupDeletions = b + return value, nil } return "", errors.New("Unknown session property") diff --git a/telegabber.go b/telegabber.go index 732a6b7..21334d2 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.10.0" +var version string = "1.10.1" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/handlers.go b/telegram/handlers.go index 1ccd622..8d1412d 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -363,6 +363,12 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { if c.Session.IsChatIgnored(update.ChatId) { return } + if c.Session.IgnoreGroupDeletions { + chatType, chatTypeErr := c.GetChatType(update.ChatId) + if chatTypeErr == nil && (chatType == ChatTypeBasicGroup || chatType == ChatTypeSupergroup) { + return + } + } var deleteChar string if c.Session.AsciiArrows { From 0368b8cad82e5ab50a73d0d9bfedef3688837216 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 22 Mar 2025 18:11:26 -0400 Subject: [PATCH 50/54] Ad-Hoc config editor --- Makefile | 2 +- persistence/sessions.go | 22 +++- telegabber.go | 2 +- telegram/commands.go | 27 +++-- xmpp/handlers.go | 227 +++++++++++++++++++++++++++++----------- 5 files changed, 206 insertions(+), 74 deletions(-) diff --git a/Makefile b/Makefile index 80f1aff..364fa43 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.10.1" +VERSION := "v1.10.2" MAKEOPTS := "-j4" all: diff --git a/persistence/sessions.go b/persistence/sessions.go index a5f658a..6d5c3ae 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -51,7 +51,13 @@ type Session struct { IgnoreGroupDeletions bool `yaml:":ignoregroupdeletions"` } -var configKeys = []string{ +const ( + PropertyTypeUnknown byte = iota + PropertyTypeString + PropertyTypeBool +) + +var ConfigKeys = []string{ "timezone", "keeponline", "rawmessages", @@ -181,7 +187,7 @@ func (s *Session) ToMap() map[string]string { defer sessionsLock.Unlock() m := make(map[string]string) - for _, configKey := range configKeys { + for _, configKey := range ConfigKeys { value, _ := s.get(configKey) m[configKey] = value } @@ -266,6 +272,18 @@ func (s *Session) Set(key string, value string) (string, error) { return "", errors.New("Unknown session property") } +// PropertyType determines the property type +func PropertyType(key string) byte { + switch key { + case "timezone": + return PropertyTypeString + case "keeponline", "rawmessages", "asciiarrows", "oobmode", "carbons", "hideids", + "receipts", "nativeedits", "ignoregroupdeletions": + return PropertyTypeBool + } + return PropertyTypeUnknown +} + // TimezoneToLocation tries to convert config timezone to location func (s *Session) TimezoneToLocation() *time.Location { time, err := time.Parse("-07:00", s.Timezone) diff --git a/telegabber.go b/telegabber.go index 21334d2..724b7d3 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.10.1" +var version string = "1.10.2" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 397ba91..71fd322 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -9,6 +9,7 @@ import ( "time" "unicode" + "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/xmpp/gateway" log "github.com/sirupsen/logrus" @@ -108,9 +109,16 @@ var chatCommands = map[string]command{ } var transportConfigurationOptions = map[string]configurationOption{ - "timezone": configurationOption{"", "adjust timezone for Telegram user statuses (example: +02:00)"}, - "keeponline": configurationOption{"", "always keep telegram session online and rely on jabber offline messages (example: true)"}, - "rawmessages": configurationOption{"", "do not add additional info (message id, origin etc.) to incoming messages (example: true)"}, + "timezone": configurationOption{"", "adjust timezone for Telegram user statuses (example: +02:00)"}, + "keeponline": configurationOption{"", "always keep telegram session online and rely on jabber offline messages (true/false)"}, + "rawmessages": configurationOption{"", "do not add additional info (message id, origin etc.) to incoming messages (true/false)"}, + "asciiarrows": configurationOption{"", "replace some Unicode symbols with ASCII alternatives for better compatibility (true/false)"}, + "oobmode": configurationOption{"", "use XEP-0066 (OOB); pros: some modern clients won't show images without it, cons: very restricted, Tkabber would flood with popups (true/false)"}, + "carbons": configurationOption{"", "send carbons to your another clients, will turn on only if supported by the server (true/false)"}, + "hideids": configurationOption{"", "hide message IDs from message info (true/false)"}, + "receipts": configurationOption{"", "if enabled, XMPP read receipts are synced to Telegram, otherwise, messages are marked as read automatically (true/false)"}, + "nativeedits": configurationOption{"", "if possible, edit XMPP messages instead of showing Telegram edits as separate messages (true/false)"}, + "ignoregroupdeletions": configurationOption{"", "suppress message deletion messages in group chats (true/false)"}, } type command struct { @@ -222,7 +230,8 @@ func (c *Client) helpString(typ CommandType, chatId int64) string { if typ == CommandTypeTransport { str.WriteString("Configuration options\n") - for name, option := range transportConfigurationOptions { + for _, name := range persistence.ConfigKeys { + option := transportConfigurationOptions[name] str.WriteString(name) str.WriteString(" ") str.WriteString(option.arguments) @@ -461,7 +470,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } case "config": if len(args) > 1 { - var msg string if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { return "The server did not allow to enable carbons", false } @@ -472,7 +480,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } gateway.DirtySessions = true - return fmt.Sprintf("%s%s set to %s", msg, args[0], value), true + return fmt.Sprintf("%s set to %s", args[0], value), true } else if len(args) > 0 { value, err := c.Session.Get(args[0]) if err != nil { @@ -483,7 +491,12 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } var entries []string - for key, value := range c.Session.ToMap() { + for _, key := range persistence.ConfigKeys { + value, err := c.Session.Get(key) + if err != nil { + log.Errorf("Achtung! Programming error in sessions with key %v", key) + continue + } entries = append(entries, fmt.Sprintf("%s is set to %s", key, value)) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index aed08d9..dcc2079 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -877,86 +877,187 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command cmdType = telegram.CommandTypeTransport } if form != nil { - // just for the case the client messed the order somehow - sort.Slice(form.Fields, func(i int, j int) bool { - iField := form.Fields[i] - jField := form.Fields[j] - if iField != nil && jField != nil { - ii, iErr := strconv.ParseInt(iField.Var, 10, 64) - ji, jErr := strconv.ParseInt(jField.Var, 10, 64) - return iErr == nil && jErr == nil && ii < ji - } - return false - }) + if command.Node == "config" { + session, ok := sessions[bare] + if ok { + var infoStrings []string + var warnString, errString string + for _, field := range form.Fields { + if len(field.ValuesList) > 0 { + fieldValue := field.ValuesList[0] - var cmd strings.Builder - cmd.WriteString("/") - cmd.WriteString(command.Node) - for _, field := range form.Fields { - cmd.WriteString(" ") - if len(field.ValuesList) > 0 { - cmd.WriteString(field.ValuesList[0]) + if gateway.MessageOutgoingPermissionVersion == 0 && field.Var == "carbons" && fieldValue == "true" { + warnString = "The server did not allow to enable carbons" + continue + } + + // 10. In accordance with Section 3.2.2.1 of XML Schema Part 2: Datatypes, the allowable + // lexical representations for the xs:boolean datatype are the strings "0" and "false" + // for the concept 'false' and the strings "1" and "true" for the concept 'true'; + // implementations MUST support both styles of lexical representation. + if persistence.PropertyType(field.Var) == persistence.PropertyTypeBool { + if fieldValue == "0" { + fieldValue = "false" + } + if fieldValue == "1" { + fieldValue = "true" + } + } + + oldValue, err := session.Session.Get(field.Var) + if err != nil || oldValue != fieldValue { + value, err := session.Session.Set(field.Var, fieldValue) + if err != nil { + errString = fmt.Sprintf("Error for field %v: %v, aborting", field.Var, err.Error()) + break + } + infoStrings = append(infoStrings, fmt.Sprintf("%s set to %s", field.Var, value)) + gateway.DirtySessions = true + } + } + } + + var elements []stanza.CommandElement + if errString != "" { + elements = append(elements, &stanza.Note{ + Text: errString, + Type: stanza.CommandNoteTypeErr, + }) + } + if warnString != "" { + elements = append(elements, &stanza.Note{ + Text: warnString, + Type: stanza.CommandNoteTypeWarn, + }) + } + for _, infoString := range infoStrings { + elements = append(elements, &stanza.Note{ + Text: infoString, + Type: stanza.CommandNoteTypeInfo, + }) + } + + answer.Payload = &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCompleted, + CommandElements: elements, + } } + } else { + // just for the case the client messed the order somehow + sort.Slice(form.Fields, func(i int, j int) bool { + iField := form.Fields[i] + jField := form.Fields[j] + if iField != nil && jField != nil { + ii, iErr := strconv.ParseInt(iField.Var, 10, 64) + ji, jErr := strconv.ParseInt(jField.Var, 10, 64) + return iErr == nil && jErr == nil && ii < ji + } + return false + }) + + var cmd strings.Builder + cmd.WriteString("/") + cmd.WriteString(command.Node) + for _, field := range form.Fields { + cmd.WriteString(" ") + if len(field.ValuesList) > 0 { + cmd.WriteString(field.ValuesList[0]) + } + } + + cmdString = cmd.String() } - - cmdString = cmd.String() } else { if command.Action == "" || command.Action == stanza.CommandActionExecute { cmd, ok := telegram.GetCommand(cmdType, command.Node) if ok && len(cmd.Arguments) > 0 { var fields []*stanza.Field - for i, arg := range cmd.Arguments { - var required *string - if i < cmd.RequiredArgs { - dummyString := "" - required = &dummyString - } + if command.Node == "config" { + session, ok := sessions[bare] + if ok { + for _, key := range persistence.ConfigKeys { + // no reason to display the item if carbons won't work + if key == "carbons" && gateway.MessageOutgoingPermissionVersion == 0 { + continue + } - var fieldType string - var options []stanza.Option - if toOk && i == 0 { - switch command.Node { - case "mute", "kick", "ban", "promote", "unmute", "unban": - session, ok := sessions[bare] - if ok { - var membersList telegram.MembersList - switch command.Node { - case "unmute": - membersList = telegram.MembersListRestricted - case "unban": - membersList = telegram.MembersListBannedAndAdministrators - } - members, err := session.GetChatMembers(toId, true, "", membersList) - if err == nil { - fieldType = stanza.FieldTypeListSingle + value, err := session.Session.Get(key) + if err != nil { + log.Errorf("Achtung! Programming error in sessions with key %v", key) + continue + } + + var fieldType string + if persistence.PropertyType(key) == persistence.PropertyTypeBool { + fieldType = stanza.FieldTypeBool + } + + field := stanza.Field{ + Var: key, + Label: key, + Type: fieldType, + ValuesList: []string{value}, + } + fields = append(fields, &field) + log.Debugf("field: %#v", field) + } + } + } else { + for i, arg := range cmd.Arguments { + var required *string + if i < cmd.RequiredArgs { + dummyString := "" + required = &dummyString + } + + var fieldType string + var options []stanza.Option + if toOk && i == 0 { + switch command.Node { + case "mute", "kick", "ban", "promote", "unmute", "unban": + session, ok := sessions[bare] + if ok { + var membersList telegram.MembersList switch command.Node { - // allow empty form - case "mute", "unmute": - options = append(options, stanza.Option{ - ValuesList: []string{""}, - }) + case "unmute": + membersList = telegram.MembersListRestricted + case "unban": + membersList = telegram.MembersListBannedAndAdministrators } - for _, member := range members { - senderId := session.GetSenderId(member.MemberId) - options = append(options, stanza.Option{ - Label: session.FormatContact(senderId), - ValuesList: []string{strconv.FormatInt(senderId, 10)}, - }) + members, err := session.GetChatMembers(toId, true, "", membersList) + if err == nil { + fieldType = stanza.FieldTypeListSingle + switch command.Node { + // allow empty form + case "mute", "unmute": + options = append(options, stanza.Option{ + ValuesList: []string{""}, + }) + } + for _, member := range members { + senderId := session.GetSenderId(member.MemberId) + options = append(options, stanza.Option{ + Label: session.FormatContact(senderId), + ValuesList: []string{strconv.FormatInt(senderId, 10)}, + }) + } } } } } - } - field := stanza.Field{ - Var: strconv.FormatInt(int64(i), 10), - Label: arg, - Required: required, - Type: fieldType, - Options: options, + field := stanza.Field{ + Var: strconv.FormatInt(int64(i), 10), + Label: arg, + Required: required, + Type: fieldType, + Options: options, + } + fields = append(fields, &field) + log.Debugf("field: %#v", field) } - fields = append(fields, &field) - log.Debugf("field: %#v", field) } form := stanza.Form{ Type: stanza.FormTypeForm, From 85846346d194d808f9a63b992332f6a66141c9b2 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 3 Apr 2025 18:57:13 -0400 Subject: [PATCH 51/54] Login Wizard --- persistence/sessions.go | 2 +- telegram/client.go | 13 +++++ telegram/commands.go | 95 +++++++++++++++++----------------- telegram/connect.go | 13 +++-- telegram/loginwizard.go | 82 ++++++++++++++++++++++++++++++ xmpp/handlers.go | 58 ++++++++++++++++++--- xmpp/loginwizard.go | 110 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 315 insertions(+), 58 deletions(-) create mode 100644 telegram/loginwizard.go create mode 100644 xmpp/loginwizard.go diff --git a/persistence/sessions.go b/persistence/sessions.go index 6d5c3ae..47d108b 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -278,7 +278,7 @@ func PropertyType(key string) byte { case "timezone": return PropertyTypeString case "keeponline", "rawmessages", "asciiarrows", "oobmode", "carbons", "hideids", - "receipts", "nativeedits", "ignoregroupdeletions": + "receipts", "nativeedits", "ignoregroupdeletions": return PropertyTypeBool } return PropertyTypeUnknown diff --git a/telegram/client.go b/telegram/client.go index 385ccd3..bd24adb 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -44,6 +44,10 @@ type Client struct { cache *cache.Cache online bool + loginWizard *loginWizardMetadata + + lastAuthorizationStateType string + outbox map[string]string editOutbox map[string]string @@ -75,6 +79,15 @@ type clientLocks struct { authorizerReadLock sync.Mutex authorizerWriteLock sync.Mutex + + loginWizardReadLock sync.Mutex + loginWizardWriteLock sync.Mutex +} + +type loginWizardMetadata struct { + nextStage chan string + chanBusy bool + commandSent bool } // NewClient instantiates a Telegram App diff --git a/telegram/commands.go b/telegram/commands.go index 71fd322..e1f0c8a 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -51,22 +51,22 @@ var permissionsMember = client.ChatPermissions{ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ - "help": command{0, []string{}, "help", nil}, - "login": command{1, []string{"phone"}, "sign in", nil}, - "logout": command{0, []string{}, "sign out", nil}, - "cancelauth": command{0, []string{}, "quit the signin wizard", nil}, - "code": command{1, []string{"xxxxx"}, "check one-time code", nil}, - "password": command{1, []string{"********"}, "check 2fa password", nil}, - "setusername": command{0, []string{"@username"}, "update @username", nil}, - "setname": command{1, []string{"first", "last"}, "update name", nil}, - "setbio": command{0, []string{"Lorem ipsum"}, "update about", nil}, - "setpassword": command{0, []string{"old", "new"}, "set or remove password", nil}, - "config": command{0, []string{"param", "value"}, "view or update configuration options", nil}, - "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username", nil}, - "add": command{1, []string{"@username"}, "add @username to your chat list", nil}, - "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", nil}, - "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", nil}, - "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", nil}, + "help": command{0, []string{}, "help", false, nil}, + "login": command{1, []string{"phone"}, "sign in", false, nil}, + "logout": command{0, []string{}, "sign out", true, nil}, + "cancelauth": command{0, []string{}, "quit the signin wizard", false, nil}, + "code": command{1, []string{"xxxxx"}, "check one-time code", false, nil}, + "password": command{1, []string{"********"}, "check 2fa password", false, nil}, + "setusername": command{0, []string{"@username"}, "update @username", true, nil}, + "setname": command{1, []string{"first", "last"}, "update name", true, nil}, + "setbio": command{0, []string{"Lorem ipsum"}, "update about", true, nil}, + "setpassword": command{0, []string{"old", "new"}, "set or remove password", true, nil}, + "config": command{0, []string{"param", "value"}, "view or update configuration options", false, nil}, + "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username", true, nil}, + "add": command{1, []string{"@username"}, "add @username to your chat list", true, nil}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", true, nil}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", true, nil}, } var notForGroups = []ChatType{ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} @@ -75,37 +75,37 @@ var notForPMAndBasic = []ChatType{ChatTypePrivate, ChatTypeSecret, ChatTypeBasic var onlyForSecret = []ChatType{ChatTypePrivate, ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} var chatCommands = map[string]command{ - "help": command{0, []string{}, "help", nil}, - "d": command{0, []string{"n"}, "delete your last message(s)", nil}, - "s": command{1, []string{"edited message"}, "edit your last message", nil}, - "silent": command{1, []string{"message"}, "send a message without sound", nil}, - "schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online", nil}, - "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", nil}, - "vcard": command{0, []string{}, "print vCard as text", nil}, - "add": command{1, []string{"@username"}, "add @username to your chat list", nil}, - "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", nil}, - "group": command{1, []string{"title"}, "create groupchat «title» with current user", ¬ForGroups}, - "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", nil}, - "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", nil}, - "secret": command{0, []string{}, "create secretchat with current user", ¬ForGroups}, - "search": command{0, []string{"string", "[limit]"}, "search in current chat", nil}, - "history": command{0, []string{"limit"}, "get last [limit] messages from current chat", nil}, - "block": command{0, []string{}, "blacklist current user", ¬ForGroups}, - "unblock": command{0, []string{}, "unblacklist current user", ¬ForGroups}, - "invite": command{1, []string{"id or @username"}, "add user to current chat", ¬ForPM}, - "link": command{0, []string{}, "get invite link for current chat", ¬ForPM}, - "kick": command{1, []string{"id or @username"}, "remove user from current chat", ¬ForPM}, - "mute": command{0, []string{"id or @username", "hours"}, "mute the whole chat or a user in current chat", ¬ForPMAndBasic}, - "unmute": command{0, []string{"id or @username"}, "unmute the whole chat or a user in the current chat", ¬ForPMAndBasic}, - "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", ¬ForPM}, - "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", ¬ForPM}, - "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", ¬ForPM}, - "leave": command{0, []string{}, "leave current chat", ¬ForPM}, - "leave!": command{0, []string{}, "leave current chat (for owners)", ¬ForPM}, - "ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying", &onlyForSecret}, - "close": command{0, []string{}, "close current secret chat", &onlyForSecret}, - "delete": command{0, []string{}, "delete current chat from chat list", nil}, - "members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)", nil}, + "help": command{0, []string{}, "help", false, nil}, + "d": command{0, []string{"n"}, "delete your last message(s)", true, nil}, + "s": command{1, []string{"edited message"}, "edit your last message", true, nil}, + "silent": command{1, []string{"message"}, "send a message without sound", true, nil}, + "schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online", true, nil}, + "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", true, nil}, + "vcard": command{0, []string{}, "print vCard as text", true, nil}, + "add": command{1, []string{"@username"}, "add @username to your chat list", true, nil}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil}, + "group": command{1, []string{"title"}, "create groupchat «title» with current user", true, ¬ForGroups}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", true, nil}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", true, nil}, + "secret": command{0, []string{}, "create secretchat with current user", true, ¬ForGroups}, + "search": command{0, []string{"string", "[limit]"}, "search in current chat", true, nil}, + "history": command{0, []string{"limit"}, "get last [limit] messages from current chat", true, nil}, + "block": command{0, []string{}, "blacklist current user", true, ¬ForGroups}, + "unblock": command{0, []string{}, "unblacklist current user", true, ¬ForGroups}, + "invite": command{1, []string{"id or @username"}, "add user to current chat", true, ¬ForPM}, + "link": command{0, []string{}, "get invite link for current chat", true, ¬ForPM}, + "kick": command{1, []string{"id or @username"}, "remove user from current chat", true, ¬ForPM}, + "mute": command{0, []string{"id or @username", "hours"}, "mute the whole chat or a user in current chat", true, ¬ForPMAndBasic}, + "unmute": command{0, []string{"id or @username"}, "unmute the whole chat or a user in the current chat", true, ¬ForPMAndBasic}, + "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", true, ¬ForPM}, + "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", true, ¬ForPM}, + "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", true, ¬ForPM}, + "leave": command{0, []string{}, "leave current chat", true, ¬ForPM}, + "leave!": command{0, []string{}, "leave current chat (for owners)", true, ¬ForPM}, + "ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying", true, &onlyForSecret}, + "close": command{0, []string{}, "close current secret chat", true, &onlyForSecret}, + "delete": command{0, []string{}, "delete current chat from chat list", true, nil}, + "members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)", true, nil}, } var transportConfigurationOptions = map[string]configurationOption{ @@ -125,6 +125,7 @@ type command struct { RequiredArgs int Arguments []string Description string + LoginOnly bool NotFor *[]ChatType } type configurationOption struct { diff --git a/telegram/connect.go b/telegram/connect.go index f344759..d37c5fd 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -129,6 +129,7 @@ func (c *Client) Connect(resource string) error { tdlibClient, err := client.NewClient(c.authorizer, c.options...) if err != nil { c.locks.authorizationReady.Unlock() + c.wizardStageOrPrompt("cancel", "") return errors.Wrap(err, "Couldn't initialize a Telegram client instance") } @@ -137,6 +138,8 @@ func (c *Client) Connect(resource string) error { // stage 3: if a client is succesfully created, AuthorizationStateReady is already reached log.Warn("Authorization successful!") + c.wizardStageOrPrompt("success", "") + c.me, err = c.client.GetMe() if err != nil { log.Error("Could not retrieve me info") @@ -255,6 +258,8 @@ func (c *Client) interactor() { log.Infof("Telegram authorization state: %#v", stateType) log.Debugf("%#v", state) + c.lastAuthorizationStateType = stateType + switch stateType { // stage 0: set login case client.TypeAuthorizationStateWaitPhoneNumber: @@ -262,12 +267,12 @@ func (c *Client) interactor() { if c.Session.Login != "" { c.authorizer.PhoneNumber <- c.Session.Login } else { - gateway.SendServiceMessage(c.jid, "Please, enter your Telegram login via /login 12345", c.xmpp) + c.wizardStageOrPrompt("login", "Please, enter your Telegram login via /login 12345, or use the Login Wizard via Ad-Hoc commands") } // stage 1: wait for auth code case client.TypeAuthorizationStateWaitCode: log.Warn("Waiting for authorization code...") - gateway.SendServiceMessage(c.jid, "Please, enter authorization code via /code 12345", c.xmpp) + c.wizardStageOrPrompt("code", "Please, enter authorization code via /code 12345") // stage 1b: wait for registration case client.TypeAuthorizationStateWaitRegistration: log.Warn("Waiting for full name...") @@ -275,7 +280,7 @@ func (c *Client) interactor() { // stage 2: wait for 2fa case client.TypeAuthorizationStateWaitPassword: log.Warn("Waiting for 2FA password...") - gateway.SendServiceMessage(c.jid, "Please, enter 2FA passphrase via /password 12345", c.xmpp) + c.wizardStageOrPrompt("password", "Please, enter 2FA passphrase via /password 12345") } c.locks.authorizerReadLock.Unlock() } @@ -294,6 +299,7 @@ func (c *Client) forceClose() { func (c *Client) close() { c.locks.authorizerWriteLock.Lock() if c.authorizer != nil && !c.authorizer.isClosed { + log.Debug("Closing authorizer") c.authorizer.Close() } c.locks.authorizerWriteLock.Unlock() @@ -308,6 +314,7 @@ func (c *Client) close() { } func (c *Client) cancelAuth() { + c.StopLoginWizard() c.close() c.Session.Login = "" } diff --git a/telegram/loginwizard.go b/telegram/loginwizard.go new file mode 100644 index 0000000..0e5cc9e --- /dev/null +++ b/telegram/loginwizard.go @@ -0,0 +1,82 @@ +package telegram + +import ( + "dev.narayana.im/narayana/telegabber/xmpp/gateway" + + log "github.com/sirupsen/logrus" + "github.com/zelenin/go-tdlib/client" +) + +// StartLoginWizard initiates a loginWizard object +func (c *Client) StartLoginWizard(inCommand bool) { + if c.loginWizard == nil { + c.loginWizard = &loginWizardMetadata{ + nextStage: make(chan string, 1), + commandSent: inCommand, + } + } else { + c.loginWizard.commandSent = inCommand + } +} + +// StopLoginWizard safely destroys the loginWizard object +func (c *Client) StopLoginWizard() { + c.locks.loginWizardReadLock.Lock() + c.locks.loginWizardWriteLock.Lock() + if c.loginWizard != nil { + close(c.loginWizard.nextStage) + c.loginWizard = nil + } + c.locks.loginWizardReadLock.Unlock() + c.locks.loginWizardWriteLock.Unlock() +} + +// GetLoginWizardNextStage waits for the next stage from the channel +func (c *Client) GetLoginWizardNextStage() string { + c.locks.loginWizardReadLock.Lock() + defer c.locks.loginWizardReadLock.Unlock() + + if c.loginWizard != nil { + if c.loginWizard.commandSent { + log.Debugf("waiting for nextStage...") + nextStage := <-c.loginWizard.nextStage + c.loginWizard.commandSent = false + c.loginWizard.chanBusy = false + log.Debugf("yielded stage %v", nextStage) + return nextStage + } else { + if c.lastAuthorizationStateType == client.TypeAuthorizationStateWaitPhoneNumber || + c.lastAuthorizationStateType == client.TypeAuthorizationStateClosing || + c.Session.Login == "" { + return "login" + } + switch c.lastAuthorizationStateType { + case client.TypeAuthorizationStateWaitCode: + return "code" + case client.TypeAuthorizationStateWaitPassword: + return "password" + } + } + } + + return "" +} + +func (c *Client) wizardStageOrPrompt(stage, message string) { + c.locks.loginWizardWriteLock.Lock() + if c.loginWizard == nil { + c.locks.loginWizardWriteLock.Unlock() + if message != "" { + gateway.SendServiceMessage(c.jid, message, c.xmpp) + } + } else { + if !c.loginWizard.chanBusy { + log.Debugf("writing wizard stage %v", stage) + c.loginWizard.nextStage <- stage + } else { + log.Warn("Skipping stage %v, wizard cannot keep up", stage) + } + c.loginWizard.chanBusy = true + c.locks.loginWizardWriteLock.Unlock() + } +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index dcc2079..1857787 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -679,25 +679,41 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { log.Debugf("discoItems: %#v", di) - _, ok := toToID(iq.To) + _, toOk := toToID(iq.To) if di.Node == gateway.NSCommand { answer.Payload = di chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) var cmdType telegram.CommandType - if ok { + if toOk { cmdType = telegram.CommandTypeChat } else { cmdType = telegram.CommandTypeTransport } + var isOnline bool + bare, _, ok := gateway.SplitJID(iq.From) + if ok { + session, ok := sessions[bare] + if ok { + isOnline = session.Online() + } + } + + if !(toOk || isOnline) { + di.AddItem(iq.To, "loginwizard", "Login Wizard") + } + commands := telegram.GetCommands(cmdType) for _, name := range telegram.SortedCommandKeys(commands) { command := commands[name] if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) { continue } + if !isOnline && command.LoginOnly { + continue + } di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) } } else { @@ -851,7 +867,13 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command return } - defer gateway.ResumableSend(component, answer) + cancelSend := false + + defer func() { + if !cancelSend { + gateway.ResumableSend(component, answer) + } + }() log.Debugf("command: %#v", command) @@ -938,12 +960,20 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command } answer.Payload = &stanza.Command{ - SessionId: command.Node, - Node: command.Node, - Status: stanza.CommandStatusCompleted, + SessionId: command.Node, + Node: command.Node, + Status: stanza.CommandStatusCompleted, CommandElements: elements, } } + } else if command.Node == "loginwizard" { + var session *telegram.Client + answer.Payload, cancelSend, session = loginWizardPayload(bare, form, resource) + + log.Debugf("immediate loginwizard payload: %#v", answer.Payload) + if cancelSend { + go sendLoginWizardResponse(component, answer, session) + } } else { // just for the case the client messed the order somehow sort.Slice(form.Fields, func(i int, j int) bool { @@ -1072,10 +1102,24 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command CommandElements: []stanza.CommandElement{&form}, } log.Debugf("form: %#v", form) + } else if command.Node == "loginwizard" { + var session *telegram.Client + answer.Payload, cancelSend, session = loginWizardPayload(bare, nil, resource) + + log.Debugf("immediate loginwizard payload: %#v", answer.Payload) + if cancelSend { + go sendLoginWizardResponse(component, answer, session) + } } else { cmdString = "/" + command.Node } } else if command.Action == stanza.CommandActionCancel { + if command.Node == "loginwizard" { + session, ok := sessions[bare] + if ok { + session.ProcessTransportCommand("/cancelauth", resource) + } + } answer.Payload = &stanza.Command{ SessionId: command.Node, Node: command.Node, @@ -1119,7 +1163,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command } - log.Debugf("command response: %#v", answer.Payload) + log.Debugf("command response: %#v %v", answer.Payload, cancelSend) } func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { diff --git a/xmpp/loginwizard.go b/xmpp/loginwizard.go new file mode 100644 index 0000000..28f6ac1 --- /dev/null +++ b/xmpp/loginwizard.go @@ -0,0 +1,110 @@ +package xmpp + +import ( + "fmt" + + "dev.narayana.im/narayana/telegabber/telegram" + "dev.narayana.im/narayana/telegabber/xmpp/gateway" + + log "github.com/sirupsen/logrus" + "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" +) + +func setCommandPayloadError(payload *stanza.Command, err string) { + note := stanza.Note{ + Text: err, + Type: stanza.CommandNoteTypeErr, + } + payload.Status = stanza.CommandStatusCompleted + payload.CommandElements = append(payload.CommandElements, ¬e) +} + +func loginWizardPayload(bare string, requestForm *stanza.Form, resource string) (payload *stanza.Command, cancelSend bool, returnSession *telegram.Client) { + payload = &stanza.Command{ + SessionId: "loginwizard", + Node: "loginwizard", + } + + session, ok := sessions[bare] + if ok { + returnSession = session + + if requestForm == nil { + session.StartLoginWizard(false) + cancelSend = true + } else { + if len(requestForm.Fields) != 1 { + setCommandPayloadError(payload, "Hey, don't tinker with the form!") + return + } + field := requestForm.Fields[0] + if field != nil { + if len(field.ValuesList) < 1 { + setCommandPayloadError(payload, "No value") + return + } + switch field.Var { + case "login", "code", "password": + default: + setCommandPayloadError(payload, "Unknown field") + return + } + + session.StartLoginWizard(true) + response, success := session.ProcessTransportCommand(fmt.Sprintf("/%v %v", field.Var, field.ValuesList[0]), resource) + if !success { + setCommandPayloadError(payload, response) + session.StopLoginWizard() + return + } + + cancelSend = true + } + } + } else { + setCommandPayloadError(payload, fmt.Sprintf("Session is not initialized, add the transport (%v) to contacts first", gateway.Jid.Bare())) + } + + return +} + +func sendLoginWizardResponse(component *xmpp.Component, answer *stanza.IQ, session *telegram.Client) { + payload := &stanza.Command{ + SessionId: "loginwizard", + Node: "loginwizard", + } + + nextStage := "login" + if session != nil { + nextStage = session.GetLoginWizardNextStage() + } + log.Debugf("nextStage: %v", nextStage) + + if nextStage == "cancel" { + setCommandPayloadError(payload, "Cancelled") + session.StopLoginWizard() + } else if nextStage == "success" { + payload.Status = stanza.CommandStatusCompleted + session.StopLoginWizard() + } else { + required := "" + form := stanza.Form{ + Type: stanza.FormTypeForm, + Title: "Login Wizard", + Fields: []*stanza.Field{ + &stanza.Field{ + Var: nextStage, + Label: nextStage, + Required: &required, + }, + }, + } + payload.Status = stanza.CommandStatusExecuting + payload.CommandElements = append(payload.CommandElements, &form) + } + + answer.Payload = payload + + gateway.ResumableSend(component, answer) +} From 3cac57e0f38d7c5be4a4061aabaf65c311451e92 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 17 Apr 2025 19:22:02 -0400 Subject: [PATCH 52/54] Fix password resetting --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 364fa43..4704236 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.10.2" +VERSION := "v1.11.0" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 724b7d3..e5940ff 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.10.2" +var version string = "1.11.0" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index e1f0c8a..df9f5ff 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -457,9 +457,10 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin var oldPassword string var newPassword string - // 0 or 1 argument is ignored and the password is reset - if len(args) > 1 { + if len(args) > 0 { oldPassword = args[0] + } + if len(args) > 1 { newPassword = args[1] } _, err := c.client.SetPassword(&client.SetPasswordRequest{ From 4414c147d8deab24a893d452090f7502246b8434 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 28 Apr 2025 20:46:21 -0400 Subject: [PATCH 53/54] Bot Menu via Ad-Hoc --- telegram/client.go | 2 + telegram/handlers.go | 15 ++++++- telegram/utils.go | 48 +++++++++++++++++++- xmpp/handlers.go | 104 +++++++++++++++++++++++++++++++++++++++++-- xmpp/loginwizard.go | 9 ---- 5 files changed, 162 insertions(+), 16 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index bd24adb..daaf627 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -58,6 +58,8 @@ type Client struct { lastMsgIds map[int64]string msgHashSeed maphash.Seed + LastBotCmdString string + XmppClientFeatures map[string]*[]string XmppClientFeaturesLock sync.Mutex diff --git a/telegram/handlers.go b/telegram/handlers.go index 8d1412d..d1e4dce 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -247,10 +247,23 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) + var forceCmd bool + if c.LastBotCmdString != "" && update.Message.IsOutgoing { + if update.Message.Content.MessageContentType() == client.TypeMessageText { + textMessage, _ := update.Message.Content.(*client.MessageText) + + if textMessage.Text != nil && textMessage.Text.Text == c.LastBotCmdString { + forceCmd = true + c.LastBotCmdString = "" + } + } + } + // ignore self outgoing messages if update.Message.IsOutgoing && update.Message.SendingState != nil && - update.Message.SendingState.MessageSendingStateType() == client.TypeMessageSendingStatePending { + update.Message.SendingState.MessageSendingStateType() == client.TypeMessageSendingStatePending && + !forceCmd { return } diff --git a/telegram/utils.go b/telegram/utils.go index 98d9d11..b0d1dd1 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -47,6 +47,16 @@ type messageStub struct { Text string } +type BotCommand struct { + Command string + Description string +} + +type BotLink struct { + Description string + Link string +} + const ( typeFileDataSha1 byte = iota typeFileDataBase64 @@ -211,6 +221,40 @@ func (c *Client) IsPM(id int64) (bool, error) { return false, nil } +// IsBot checks if a chat is a bot +func (c *Client) IsBot(id int64) (bool, error) { + _, user, err := c.GetContactByID(id, nil) + if err != nil { + return false, err + } + if user == nil || user.Type == nil { + return false, nil + } + + return user.Type.UserTypeType() == client.TypeUserTypeBot, nil +} + +// GetBotMenu retrieves the bot's attachment menu +func (c *Client) GetBotMenu(id int64) (*BotLink, []*BotCommand, error) { + fullInfo, err := c.client.GetUserFullInfo(&client.GetUserFullInfoRequest{ + UserId: id, + }) + if err == nil && fullInfo.BotInfo != nil { + if fullInfo.BotInfo.MenuButton != nil { + menuButton := fullInfo.BotInfo.MenuButton + return &BotLink{menuButton.Text, menuButton.Url}, nil, nil + } else { + var commands []*BotCommand + for _, command := range fullInfo.BotInfo.Commands { + commands = append(commands, &BotCommand{command.Command, command.Description}) + } + return nil, commands, nil + } + } + + return nil, nil, err +} + func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { var show, textStatus, presenceType string @@ -1275,13 +1319,13 @@ func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageC } // ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id -func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64) int64 { +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64, raw bool) int64 { if !c.Online() { // we're offline return 0 } - if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { + if replaceId == 0 && !raw && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands response, isCommand, _ := c.ProcessChatCommand(chatID, text) if response != "" { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1857787..49c41fb 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -204,7 +204,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { session.SendMessageLock.Lock() defer session.SendMessageLock.Unlock() - tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) + tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, false) if tgMessageId != 0 { if replaceId != 0 { // not needed (is it persistent among clients though?) @@ -679,7 +679,7 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { log.Debugf("discoItems: %#v", di) - _, toOk := toToID(iq.To) + toID, toOk := toToID(iq.To) if di.Node == gateway.NSCommand { answer.Payload = di @@ -698,6 +698,13 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { session, ok := sessions[bare] if ok { isOnline = session.Online() + + if toOk { + isBot, err := session.IsBot(toID) + if err == nil && isBot { + di.AddItem(iq.To, "botmenu", "Bot Menu") + } + } } } @@ -966,7 +973,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command CommandElements: elements, } } - } else if command.Node == "loginwizard" { + } else if !toOk && command.Node == "loginwizard" { var session *telegram.Client answer.Payload, cancelSend, session = loginWizardPayload(bare, form, resource) @@ -974,6 +981,31 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if cancelSend { go sendLoginWizardResponse(component, answer, session) } + } else if toOk && command.Node == "botmenu" { + payload := &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + } + answer.Payload = payload + + if len(form.Fields) == 1 && form.Fields[0] != nil && + form.Fields[0].Var == "command" && len(form.Fields[0].ValuesList) == 1 { + session, ok := sessions[bare] + if ok { + msgText := "/"+form.Fields[0].ValuesList[0] + session.LastBotCmdString = msgText + tgMessageId := session.ProcessOutgoingMessage(toId, msgText, iq.From, 0, 0, true) + if tgMessageId != 0 { + payload.Status = stanza.CommandStatusCompleted + } else { + setCommandPayloadError(payload, "Failed to send a bot command") + } + } else { + setCommandPayloadError(payload, "Session is lost") + } + } else { + setCommandPayloadError(payload, "Broken form") + } } else { // just for the case the client messed the order somehow sort.Slice(form.Fields, func(i int, j int) bool { @@ -1102,7 +1134,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command CommandElements: []stanza.CommandElement{&form}, } log.Debugf("form: %#v", form) - } else if command.Node == "loginwizard" { + } else if !toOk && command.Node == "loginwizard" { var session *telegram.Client answer.Payload, cancelSend, session = loginWizardPayload(bare, nil, resource) @@ -1110,6 +1142,61 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if cancelSend { go sendLoginWizardResponse(component, answer, session) } + } else if toOk && command.Node == "botmenu" { + session, ok := sessions[bare] + + var link *telegram.BotLink + var commands []*telegram.BotCommand + var err error + if ok { + link, commands, err = session.GetBotMenu(toId) + } + + payload := &stanza.Command{ + SessionId: command.Node, + Node: command.Node, + } + answer.Payload = payload + + if !ok || err != nil { + setCommandPayloadError(payload, "Cannot retrieve commands") + } else { + if link != nil { + payload.Status = stanza.CommandStatusCompleted + payload.CommandElements = []stanza.CommandElement{ + &stanza.Note{ + Text: fmt.Sprintf("%v: %v", link.Description, link.Link), + Type: stanza.CommandNoteTypeInfo, + }, + } + } else { + var options []stanza.Option + for _, cmd := range commands { + options = append(options, stanza.Option{ + Label: fmt.Sprintf("/%v — %v", cmd.Command, cmd.Description), + ValuesList: []string{cmd.Command}, + }) + } + + dummyString := "" + field := stanza.Field{ + Var: "command", + Type: stanza.FieldTypeListSingle, + Required: &dummyString, + Options: options, + } + log.Debugf("field: %#v", field) + + form := stanza.Form{ + Type: stanza.FormTypeForm, + Fields: []*stanza.Field{&field}, + } + log.Debugf("form: %#v", form) + + payload.Status = stanza.CommandStatusExecuting + payload.CommandElements = []stanza.CommandElement{&form} + } + } } else { cmdString = "/" + command.Node } @@ -1206,6 +1293,15 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code } } +func setCommandPayloadError(payload *stanza.Command, err string) { + note := stanza.Note{ + Text: err, + Type: stanza.CommandNoteTypeErr, + } + payload.Status = stanza.CommandStatusCompleted + payload.CommandElements = append(payload.CommandElements, ¬e) +} + func probeClientFeatures(jid string, component *xmpp.Component) { id, err := uuid.NewRandom() if err != nil { diff --git a/xmpp/loginwizard.go b/xmpp/loginwizard.go index 28f6ac1..d1b161a 100644 --- a/xmpp/loginwizard.go +++ b/xmpp/loginwizard.go @@ -11,15 +11,6 @@ import ( "gosrc.io/xmpp/stanza" ) -func setCommandPayloadError(payload *stanza.Command, err string) { - note := stanza.Note{ - Text: err, - Type: stanza.CommandNoteTypeErr, - } - payload.Status = stanza.CommandStatusCompleted - payload.CommandElements = append(payload.CommandElements, ¬e) -} - func loginWizardPayload(bare string, requestForm *stanza.Form, resource string) (payload *stanza.Command, cancelSend bool, returnSession *telegram.Client) { payload = &stanza.Command{ SessionId: "loginwizard", From 9378fa4991fe277d7dd57956beedfb782a2d242a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 30 Apr 2025 19:55:34 -0400 Subject: [PATCH 54/54] Add /raw command to bypass bot commands --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 16 ++++++++++++++++ xmpp/handlers.go | 4 ++-- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4704236..8a3b062 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.11.0" +VERSION := "v1.12.0" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index e5940ff..1f0f3e0 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.11.0" +var version string = "1.12.0" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index df9f5ff..ec1db89 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -80,6 +80,7 @@ var chatCommands = map[string]command{ "s": command{1, []string{"edited message"}, "edit your last message", true, nil}, "silent": command{1, []string{"message"}, "send a message without sound", true, nil}, "schedule": command{2, []string{"{online | 2006-01-02T15:04:05 | 15:04:05}", "message"}, "schedules a message either to timestamp or to whenever the user goes online", true, nil}, + "raw": command{1, []string{"message"}, "send a raw message not interpeted as a transport command (e.g. a bot command)", true, nil}, "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", true, nil}, "vcard": command{0, []string{}, "print vCard as text", true, nil}, "add": command{1, []string{"@username"}, "add @username to your chat list", true, nil}, @@ -725,6 +726,21 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } else { return "Message processing error", true, false } + // sends a raw non-interpreted message + case "raw": + content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) + + if content != nil { + _, err := c.client.SendMessage(&client.SendMessageRequest{ + ChatId: chatID, + InputMessageContent: content, + }) + if err != nil { + return err.Error(), true, false + } + } else { + return "Message processing error", true, false + } // forward a message to chat case "forward": messageId, err := strconv.ParseInt(args[0], 10, 64) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 49c41fb..c5560ec 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -989,10 +989,10 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command answer.Payload = payload if len(form.Fields) == 1 && form.Fields[0] != nil && - form.Fields[0].Var == "command" && len(form.Fields[0].ValuesList) == 1 { + form.Fields[0].Var == "command" && len(form.Fields[0].ValuesList) == 1 { session, ok := sessions[bare] if ok { - msgText := "/"+form.Fields[0].ValuesList[0] + msgText := "/" + form.Fields[0].ValuesList[0] session.LastBotCmdString = msgText tgMessageId := session.ProcessOutgoingMessage(toId, msgText, iq.From, 0, 0, true) if tgMessageId != 0 {