From afa21e10be45a6186b99a33e4d8f22d820677efb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 7 Jul 2022 18:23:50 -0400 Subject: [PATCH 001/228] Add a muc option (useless yet) --- persistence/sessions.go | 11 +++++++++++ persistence/sessions_test.go | 2 ++ 2 files changed, 13 insertions(+) diff --git a/persistence/sessions.go b/persistence/sessions.go index bc71fd6..18d748e 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -39,6 +39,7 @@ type Session struct { KeepOnline bool `yaml:":keeponline"` RawMessages bool `yaml:":rawmessages"` AsciiArrows bool `yaml:":asciiarrows"` + MUC bool `yaml:":muc"` } var configKeys = []string{ @@ -46,6 +47,7 @@ var configKeys = []string{ "keeponline", "rawmessages", "asciiarrows", + "muc", } var sessionDB *SessionsYamlDB @@ -118,6 +120,8 @@ func (s *Session) Get(key string) (string, error) { return fromBool(s.RawMessages), nil case "asciiarrows": return fromBool(s.AsciiArrows), nil + case "muc": + return fromBool(s.MUC), nil } return "", errors.New("Unknown session property") @@ -161,6 +165,13 @@ func (s *Session) Set(key string, value string) (string, error) { } s.AsciiArrows = b return value, nil + case "muc": + b, err := toBool(value) + if err != nil { + return "", err + } + s.MUC = b + return value, nil } return "", errors.New("Unknown session property") diff --git a/persistence/sessions_test.go b/persistence/sessions_test.go index 76f71f9..c553e95 100644 --- a/persistence/sessions_test.go +++ b/persistence/sessions_test.go @@ -47,11 +47,13 @@ func TestSessionToMap(t *testing.T) { session := Session{ Timezone: "klsf", RawMessages: true, + MUC: true, } m := session.ToMap() sample := map[string]string{ "timezone": "klsf", "keeponline": "false", + "muc": "true", "rawmessages": "true", "asciiarrows": "false", } From 6abb7ff9c238f0343351c15c09210d746086f5f6 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 7 Jul 2022 20:38:06 -0400 Subject: [PATCH 002/228] Respond to disco with conference identity and groups list --- telegram/utils.go | 24 ++++++++++++++++++++ xmpp/handlers.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index 17e9861..7747acb 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1065,6 +1065,30 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { return "" } +// GetGroupChats obtains all group chats +func (c *Client) GetGroupChats() []*client.Chat { + var groupChats []*client.Chat + + chats, err := c.client.GetChats(&client.GetChatsRequest{ + Limit: chatsLimit, + }) + if err == nil { + for _, id := range chats.ChatIds { + chat, _, _ := c.GetContactByID(id, nil) + if chat != nil { + typ := chat.Type.ChatTypeType() + if typ == client.TypeChatTypeBasicGroup { + groupChats = append(groupChats, chat) + } + } + } + } else { + log.Errorf("Could not retrieve chats: %v", err) + } + + return groupChats +} + // subscribe to a Telegram ID func (c *Client) subscribeToID(id int64, chat *client.Chat) { var args []args.V diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 7c671d9..64915a1 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -55,6 +55,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetDiscoInfo(s, iq) return } + _, ok = iq.Payload.(*stanza.DiscoItems) + if ok { + go handleGetDiscoItems(s, iq) + return + } } } @@ -333,6 +338,59 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") } + bare, _, ok := splitFrom(iq.From) + if ok { + session, ok := sessions[bare] + if ok && session.Session.MUC { + disco.AddFeatures(stanza.NSDiscoItems) + disco.AddIdentity("Telegram group chats", "conference", "text") + } + } + answer.Payload = disco + + log.Debugf("%#v", answer) + + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + _ = gateway.ResumableSend(component, answer) +} + +func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { + 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 + } + + disco := answer.DiscoItems() + + _, ok := toToID(iq.To) + if !ok { + bare, _, ok := splitFrom(iq.From) + if ok { + // raw access, no need to create a new instance if not connected + session, ok := sessions[bare] + if ok && session.Session.MUC { + bareJid := gateway.Jid.Bare() + disco.AddItem(bareJid, "", "Telegram group chats") + for _, chat := range session.GetGroupChats() { + jid := strconv.FormatInt(chat.Id, 10) + "@" + bareJid + disco.AddItem(jid, "", chat.Title) + } + } + } + } + answer.Payload = disco log.Debugf("%#v", answer) From 63f12202d0c5d2a570b48d05e62722e0fea54467 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 8 Jul 2022 07:54:30 -0400 Subject: [PATCH 003/228] Refactoring: merge handleGetDiscoInfo/handleGetDiscoItems back into one function --- xmpp/handlers.go | 94 ++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 64915a1..3b688f5 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -24,6 +24,12 @@ const ( ) const NodeVCard4 string = "urn:xmpp:vcard4" +type discoType int +const ( + discoTypeInfo discoType = iota + discoTypeItems +) + func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) } @@ -52,12 +58,12 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { } _, ok = iq.Payload.(*stanza.DiscoInfo) if ok { - go handleGetDiscoInfo(s, iq) + go handleGetDisco(discoTypeInfo, s, iq) return } _, ok = iq.Payload.(*stanza.DiscoItems) if ok { - go handleGetDiscoItems(s, iq) + go handleGetDisco(discoTypeItems, s, iq) return } } @@ -318,7 +324,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { _ = gateway.ResumableSend(component, &answer) } -func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { +func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, @@ -331,67 +337,45 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { return } - disco := answer.DiscoInfo() - _, ok := toToID(iq.To) - if ok { - disco.AddIdentity("", "account", "registered") - } else { - disco.AddIdentity("Telegram Gateway", "gateway", "telegram") - } - bare, _, ok := splitFrom(iq.From) - if ok { - session, ok := sessions[bare] - if ok && session.Session.MUC { - disco.AddFeatures(stanza.NSDiscoItems) - disco.AddIdentity("Telegram group chats", "conference", "text") + if dt == discoTypeInfo { + disco := answer.DiscoInfo() + _, ok := toToID(iq.To) + if ok { + disco.AddIdentity("", "account", "registered") + } else { + disco.AddIdentity("Telegram Gateway", "gateway", "telegram") } - } - answer.Payload = disco - - log.Debugf("%#v", answer) - - component, ok := s.(*xmpp.Component) - if !ok { - log.Error("Not a component") - return - } - - _ = gateway.ResumableSend(component, answer) -} - -func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { - 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 - } - - disco := answer.DiscoItems() - - _, ok := toToID(iq.To) - if !ok { bare, _, ok := splitFrom(iq.From) if ok { - // raw access, no need to create a new instance if not connected session, ok := sessions[bare] if ok && session.Session.MUC { - bareJid := gateway.Jid.Bare() - disco.AddItem(bareJid, "", "Telegram group chats") - for _, chat := range session.GetGroupChats() { - jid := strconv.FormatInt(chat.Id, 10) + "@" + bareJid - disco.AddItem(jid, "", chat.Title) + disco.AddFeatures(stanza.NSDiscoItems) + disco.AddIdentity("Telegram group chats", "conference", "text") + } + } + answer.Payload = disco + } else if dt == discoTypeItems { + disco := answer.DiscoItems() + + _, ok := toToID(iq.To) + if !ok { + bare, _, ok := splitFrom(iq.From) + if ok { + // raw access, no need to create a new instance if not connected + session, ok := sessions[bare] + if ok && session.Session.MUC { + bareJid := gateway.Jid.Bare() + disco.AddItem(bareJid, "", "Telegram group chats") + for _, chat := range session.GetGroupChats() { + jid := strconv.FormatInt(chat.Id, 10) + "@" + bareJid + disco.AddItem(jid, "", chat.Title) + } } } } - } - answer.Payload = disco + answer.Payload = disco + } log.Debugf("%#v", answer) From 7ef32096af6017607f70a0b5aa2fbc03925c72d1 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 8 Jul 2022 08:43:44 -0400 Subject: [PATCH 004/228] Basic room disco info --- telegram/utils.go | 13 ++++++++----- xmpp/handlers.go | 33 +++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 7747acb..5244803 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1075,11 +1075,8 @@ func (c *Client) GetGroupChats() []*client.Chat { if err == nil { for _, id := range chats.ChatIds { chat, _, _ := c.GetContactByID(id, nil) - if chat != nil { - typ := chat.Type.ChatTypeType() - if typ == client.TypeChatTypeBasicGroup { - groupChats = append(groupChats, chat) - } + if chat != nil && c.IsGroup(chat) { + groupChats = append(groupChats, chat) } } } else { @@ -1089,6 +1086,12 @@ func (c *Client) GetGroupChats() []*client.Chat { return groupChats } +// IsGroup determines if a chat is eligible to be represented as MUC +func (c *Client) IsGroup(chat *client.Chat) bool { + typ := chat.Type.ChatTypeType() + return typ == client.TypeChatTypeBasicGroup +} + // subscribe to a Telegram ID func (c *Client) subscribeToID(id int64, chat *client.Chat) { var args []args.V diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 3b688f5..6531983 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -339,18 +339,35 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { if dt == discoTypeInfo { disco := answer.DiscoInfo() - _, ok := toToID(iq.To) - if ok { + toID, toOk := toToID(iq.To) + if toOk { disco.AddIdentity("", "account", "registered") } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") } - bare, _, ok := splitFrom(iq.From) - if ok { - session, ok := sessions[bare] - if ok && session.Session.MUC { - disco.AddFeatures(stanza.NSDiscoItems) - disco.AddIdentity("Telegram group chats", "conference", "text") + bare, _, fromOk := splitFrom(iq.From) + if fromOk { + session, sessionOk := sessions[bare] + if sessionOk && session.Session.MUC { + if toOk { + chat, _, err := session.GetContactByID(toID, nil) + if err == nil && session.IsGroup(chat) { + disco.AddIdentity(chat.Title, "conference", "text") + } + + disco.AddFeatures( + "http://jabber.org/protocol/muc", + "muc_persistent", + "muc_hidden", + "muc_membersonly", + "muc_unmoderated", + "muc_nonanonymous", + "muc_unsecured", + ) + } else { + disco.AddFeatures(stanza.NSDiscoItems) + disco.AddIdentity("Telegram group chats", "conference", "text") + } } } answer.Payload = disco From 63521b8f90af65cad9ca7510be3c2b76307d8090 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 8 Jul 2022 17:43:56 -0400 Subject: [PATCH 005/228] Extended room disco info --- go.mod | 2 +- go.sum | 2 ++ telegram/utils.go | 35 ++++++++++++++++++++++++++++--- xmpp/handlers.go | 53 +++++++++++++++++++++++++++++++++-------------- 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 41f4e67..ffbf56d 100644 --- a/go.mod +++ b/go.mod @@ -13,4 +13,4 @@ require ( gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 ) -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-20220708184440-35d9cd68e55f diff --git a/go.sum b/go.sum index 5fa5f81..3e9c97e 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829 h1:qe81G6+t1 dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= 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-20220708184440-35d9cd68e55f h1:aT50UsPH1dLje9CCAquRRhr7I9ZvL3kQU6WIWTe8PZ0= +dev.narayana.im/narayana/go-xmpp v0.0.0-20220708184440-35d9cd68e55f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 h1:GbV1Lv3lVHsSeKAqPTBem72OCsGjXntW4jfJdXciE+w= github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7/go.mod h1:ZzkRfuaFj8etIYMj/ECtXtgfz72RE6U+dos27b3XIwk= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= diff --git a/telegram/utils.go b/telegram/utils.go index 5244803..377b784 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1039,7 +1039,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { return fullInfo.Description } } else { - log.Warnf("Coudln't retrieve private chat info: %v", err.Error()) + log.Warnf("Couldn't retrieve private chat info: %v", err.Error()) } } else if chatType == client.TypeChatTypeBasicGroup { basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) @@ -1049,7 +1049,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { if err == nil { return fullInfo.Description } else { - log.Warnf("Coudln't retrieve basic group info: %v", err.Error()) + log.Warnf("Couldn't retrieve basic group info: %v", err.Error()) } } else if chatType == client.TypeChatTypeSupergroup { supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) @@ -1059,12 +1059,41 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { if err == nil { return fullInfo.Description } else { - log.Warnf("Coudln't retrieve supergroup info: %v", err.Error()) + log.Warnf("Couldn't retrieve supergroup info: %v", err.Error()) } } return "" } +// GetChatMemberCount obtains the member count depending on the chat type +func (c *Client) GetChatMemberCount(chat *client.Chat) int32 { + chatType := chat.Type.ChatTypeType() + if chatType == client.TypeChatTypePrivate { + return 2 + } else if chatType == client.TypeChatTypeBasicGroup { + basicGroupType, _ := chat.Type.(*client.ChatTypeBasicGroup) + basicGroup, err := c.client.GetBasicGroup(&client.GetBasicGroupRequest{ + BasicGroupId: basicGroupType.BasicGroupId, + }) + if err == nil { + return basicGroup.MemberCount + } else { + log.Warnf("Couldn't retrieve basic group: %v", err.Error()) + } + } else if chatType == client.TypeChatTypeSupergroup { + supergroupType, _ := chat.Type.(*client.ChatTypeSupergroup) + supergroup, err := c.client.GetSupergroup(&client.GetSupergroupRequest{ + SupergroupId: supergroupType.SupergroupId, + }) + if err == nil { + return supergroup.MemberCount + } else { + log.Warnf("Couldn't retrieve supergroup: %v", err.Error()) + } + } + return 0 +} + // GetGroupChats obtains all group chats func (c *Client) GetGroupChats() []*client.Chat { var groupChats []*client.Chat diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 6531983..fde7382 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -340,11 +340,7 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { if dt == discoTypeInfo { disco := answer.DiscoInfo() toID, toOk := toToID(iq.To) - if toOk { - disco.AddIdentity("", "account", "registered") - } else { - disco.AddIdentity("Telegram Gateway", "gateway", "telegram") - } + var isMuc bool bare, _, fromOk := splitFrom(iq.From) if fromOk { session, sessionOk := sessions[bare] @@ -352,24 +348,51 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { if toOk { chat, _, err := session.GetContactByID(toID, nil) if err == nil && session.IsGroup(chat) { + isMuc = true disco.AddIdentity(chat.Title, "conference", "text") - } - disco.AddFeatures( - "http://jabber.org/protocol/muc", - "muc_persistent", - "muc_hidden", - "muc_membersonly", - "muc_unmoderated", - "muc_nonanonymous", - "muc_unsecured", - ) + disco.AddFeatures( + "http://jabber.org/protocol/muc", + "muc_persistent", + "muc_hidden", + "muc_membersonly", + "muc_unmoderated", + "muc_nonanonymous", + "muc_unsecured", + ) + fields := []*stanza.Field{ + &stanza.Field{ + Var: "FORM_TYPE", + Type: "hidden", + ValuesList: []string{"http://jabber.org/protocol/muc#roominfo"}, + }, + &stanza.Field{ + Var: "muc#roominfo_description", + Label: "Description", + ValuesList: []string{session.GetChatDescription(chat)}, + }, + &stanza.Field{ + Var: "muc#roominfo_occupants", + Label: "Number of occupants", + ValuesList: []string{strconv.FormatInt(int64(session.GetChatMemberCount(chat)), 10)}, + }, + } + + disco.Form = stanza.NewForm(fields, "result") + } } else { disco.AddFeatures(stanza.NSDiscoItems) disco.AddIdentity("Telegram group chats", "conference", "text") } } } + if toOk { + if !isMuc { + disco.AddIdentity("", "account", "registered") + } + } else { + disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + } answer.Payload = disco } else if dt == discoTypeItems { disco := answer.DiscoItems() From 7eaf28ad7c4d2bdf5aa6313503d751de90a6811c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 8 Jul 2022 17:59:51 -0400 Subject: [PATCH 006/228] Advertise gateway first, MUC next --- xmpp/handlers.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index fde7382..0de33b8 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -340,6 +340,10 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { if dt == discoTypeInfo { disco := answer.DiscoInfo() toID, toOk := toToID(iq.To) + if !toOk { + disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + } + var isMuc bool bare, _, fromOk := splitFrom(iq.From) if fromOk { @@ -386,12 +390,8 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { } } } - if toOk { - if !isMuc { - disco.AddIdentity("", "account", "registered") - } - } else { - disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + if toOk && !isMuc { + disco.AddIdentity("", "account", "registered") } answer.Payload = disco } else if dt == discoTypeItems { From 8663a29e157aae6e68cc880a86b3a666da37bfc9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 1 Jun 2023 16:37:38 -0400 Subject: [PATCH 007/228] Add /vcard command --- telegabber.go | 2 +- telegram/commands.go | 20 +++++++ telegram/utils.go | 129 ++++++++++++++++++++++++++++++++++++------- xmpp/handlers.go | 106 ++++++++++++++++------------------- 4 files changed, 177 insertions(+), 80 deletions(-) diff --git a/telegabber.go b/telegabber.go index 72353bb..c8d6a8f 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.5.0" +var version string = "1.6.0-dev" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 2a72219..ec06f36 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -64,6 +64,7 @@ var chatCommands = map[string]command{ "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"}, @@ -172,6 +173,10 @@ func rawCmdArguments(cmdline string, start uint8) string { return "" } +func keyValueString(key, value string) string { + return fmt.Sprintf("%s: %s", key, value) +} + func (c *Client) unsubscribe(chatID int64) error { return gateway.SendPresence( c.xmpp, @@ -636,6 +641,21 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) c.ProcessIncomingMessage(targetChatId, message) } } + // print vCard + case "vcard": + info, err := c.GetVcardInfo(chatID) + if err != nil { + return err.Error(), true + } + _, link := c.PermastoreFile(info.Photo, true) + entries := []string{ + keyValueString("Chat title", info.Fn), + keyValueString("Photo", link), + keyValueString("Username", info.Nickname), + keyValueString("Full name", info.Given + " " + info.Family), + keyValueString("Phone number", info.Tel), + } + return strings.Join(entries, "\n"), true // add @contact case "add": return c.cmdAdd(args), true diff --git a/telegram/utils.go b/telegram/utils.go index 9a247c4..851c6c1 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -24,6 +24,16 @@ import ( "github.com/zelenin/go-tdlib/client" ) +type VCardInfo struct { + Fn string + Photo *client.File + Nickname string + Given string + Family string + Tel string + Info string +} + var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) @@ -207,7 +217,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.OpenPhotoFile(chat.Photo.Small, 1) + file, path, err := c.ForceOpenFile(chat.Photo.Small, 1) if err == nil { defer file.Close() @@ -408,6 +418,20 @@ func (c *Client) formatForward(fwd *client.MessageForwardInfo) string { } func (c *Client) formatFile(file *client.File, compact bool) (string, string) { + if file == nil { + return "", "" + } + src, link := c.PermastoreFile(file, false) + + if compact { + return link, link + } else { + return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link + } +} + +// PermastoreFile steals a file out of TDlib control into an independent shared directory +func (c *Client) PermastoreFile(file *client.File, clone bool) (string, string) { log.Debugf("file: %#v", file) if file == nil || file.Local == nil || file.Remote == nil { return "", "" @@ -434,18 +458,57 @@ func (c *Client) formatFile(file *client.File, compact bool) (string, string) { dest := c.content.Path + "/" + basename // destination path link = c.content.Link + "/" + basename // download link - // move - err = os.Rename(src, dest) - if err != nil { - linkErr := err.(*os.LinkError) - if linkErr.Err.Error() == "file exists" { - log.Warn(err.Error()) + if clone { + file, path, err := c.ForceOpenFile(file, 1) + if err == nil { + defer file.Close() + + // mode + mode := os.FileMode(0644) + fi, err := os.Stat(path) + if err == nil { + mode = fi.Mode().Perm() + } + + // create destination + tempFile, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) + if err != nil { + pathErr := err.(*os.PathError) + if pathErr.Err.Error() == "file exists" { + log.Warn(err.Error()) + return src, link + } else { + log.Errorf("File creation error: %v", err) + return "", "" + } + } + defer tempFile.Close() + // copy + _, err = io.Copy(tempFile, file) + if err != nil { + log.Errorf("File copying error: %v", err) + return "", "" + } + } else if path != "" { + log.Errorf("Source file does not exist: %v", path) + return "", "" } else { - log.Errorf("File moving error: %v", err) + log.Errorf("PHOTO: %#v", err.Error()) return "", "" } + } else { + // move + err = os.Rename(src, dest) + if err != nil { + linkErr := err.(*os.LinkError) + if linkErr.Err.Error() == "file exists" { + log.Warn(err.Error()) + } else { + log.Errorf("File moving error: %v", err) + return "", "" + } + } } - gateway.CachedStorageSize += size64 // chown if c.content.User != "" { @@ -464,13 +527,12 @@ func (c *Client) formatFile(file *client.File, compact bool) (string, string) { log.Errorf("Wrong user name for chown: %v", err) } } + + // copy or move should have succeeded at this point + gateway.CachedStorageSize += size64 } - if compact { - return link, link - } else { - return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link - } + return src, link } func (c *Client) formatBantime(hours int64) int32 { @@ -1148,20 +1210,20 @@ func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*clie }) } -// OpenPhotoFile reliably obtains a photo if possible -func (c *Client) OpenPhotoFile(photoFile *client.File, priority int32) (*os.File, string, error) { - if photoFile == nil { - return nil, "", errors.New("Photo file not found") +// ForceOpenFile reliably obtains a file if possible +func (c *Client) ForceOpenFile(tgFile *client.File, priority int32) (*os.File, string, error) { + if tgFile == nil { + return nil, "", errors.New("File not found") } - path := photoFile.Local.Path + path := tgFile.Local.Path file, err := os.Open(path) if err == nil { return file, path, nil } else // obtain the photo right now if still not downloaded - if !photoFile.Local.IsDownloadingCompleted { - tdFile, tdErr := c.DownloadFile(photoFile.Id, priority, true) + if !tgFile.Local.IsDownloadingCompleted { + tdFile, tdErr := c.DownloadFile(tgFile.Id, priority, true) if tdErr == nil { path = tdFile.Local.Path file, err = os.Open(path) @@ -1248,3 +1310,28 @@ func (c *Client) prepareDiskSpace(size uint64) { } } } + +func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { + var info VCardInfo + chat, user, err := c.GetContactByID(toID, nil) + if err != nil { + return info, err + } + + if chat != nil { + info.Fn = chat.Title + + if chat.Photo != nil { + info.Photo = chat.Photo.Small + } + info.Info = c.GetChatDescription(chat) + } + if user != nil { + info.Nickname = user.Username + info.Given = user.FirstName + info.Family = user.LastName + info.Tel = user.PhoneNumber + } + + return info, nil +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index db2b6ea..780478a 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -10,6 +10,7 @@ import ( "strings" "dev.narayana.im/narayana/telegabber/persistence" + "dev.narayana.im/narayana/telegabber/telegram" "dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/gateway" @@ -319,45 +320,12 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { log.Error("Invalid IQ to") return } - chat, user, err := session.GetContactByID(toID, nil) + info, err := session.GetVcardInfo(toID) if err != nil { log.Error(err) return } - var fn, photo, nickname, given, family, tel, info string - if chat != nil { - fn = chat.Title - - if chat.Photo != nil { - file, path, err := session.OpenPhotoFile(chat.Photo.Small, 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 { - photo = 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()) - } - } - info = session.GetChatDescription(chat) - } - if user != nil { - nickname = user.Username - given = user.FirstName - family = user.LastName - tel = user.PhoneNumber - } - answer := stanza.IQ{ Attrs: stanza.Attrs{ From: iq.To, @@ -365,7 +333,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { Id: iq.Id, Type: "result", }, - Payload: makeVCardPayload(typ, iq.To, fn, photo, nickname, given, family, tel, info), + Payload: makeVCardPayload(typ, iq.To, info, session), } log.Debugf("%#v", answer) @@ -426,53 +394,75 @@ func toToID(to string) (int64, bool) { return toID, true } -func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, info string) stanza.IQPayload { +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()) + } + } + if typ == TypeVCardTemp { vcard := &extensions.IqVcardTemp{} - vcard.Fn.Text = fn - if photo != "" { + vcard.Fn.Text = info.Fn + if base64Photo != "" { vcard.Photo.Type.Text = "image/jpeg" - vcard.Photo.Binval.Text = photo + vcard.Photo.Binval.Text = base64Photo } - vcard.Nickname.Text = nickname - vcard.N.Given.Text = given - vcard.N.Family.Text = family - vcard.Tel.Number.Text = tel - vcard.Desc.Text = info + vcard.Nickname.Text = info.Nickname + vcard.N.Given.Text = info.Given + vcard.N.Family.Text = info.Family + vcard.Tel.Number.Text = info.Tel + vcard.Desc.Text = info.Info return vcard } else if typ == TypeVCard4 { nodes := []stanza.Node{} - if fn != "" { + if info.Fn != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "fn"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: fn, + Content: info.Fn, }, }, }) } - if photo != "" { + if base64Photo != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "photo"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, - Content: "data:image/jpeg;base64," + photo, + Content: "data:image/jpeg;base64," + base64Photo, }, }, }) } - if nickname != "" { + if info.Nickname != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "nickname"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: nickname, + Content: info.Nickname, }, }, }, stanza.Node{ @@ -480,44 +470,44 @@ func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, inf Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, - Content: "https://t.me/" + nickname, + Content: "https://t.me/" + info.Nickname, }, }, }) } - if family != "" || given != "" { + if info.Family != "" || info.Given != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "n"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "surname"}, - Content: family, + Content: info.Family, }, stanza.Node{ XMLName: xml.Name{Local: "given"}, - Content: given, + Content: info.Given, }, }, }) } - if tel != "" { + if info.Tel != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "tel"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, - Content: "tel:" + tel, + Content: "tel:" + info.Tel, }, }, }) } - if info != "" { + if info.Info != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "note"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: info, + Content: info.Info, }, }, }) From 9a84e9a8b6b7a6f953301e54f19cdf4be73592e1 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 3 Jun 2023 00:20:03 -0400 Subject: [PATCH 008/228] Store message ids in Badger DB --- Makefile | 2 +- README.md | 1 + badger/ids.go | 132 ++++++++++++++++++++++++++++++++++++++++ badger/ids_test.go | 72 ++++++++++++++++++++++ go.mod | 3 +- go.sum | 121 ++++++++++++++++++++++++++++++++++++ telegabber.go | 4 +- telegram/commands.go | 6 +- telegram/utils.go | 9 ++- xmpp/component.go | 10 ++- xmpp/gateway/gateway.go | 4 ++ xmpp/handlers.go | 2 +- 12 files changed, 356 insertions(+), 10 deletions(-) create mode 100644 badger/ids.go create mode 100644 badger/ids_test.go diff --git a/Makefile b/Makefile index 48c5d7e..048987d 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ all: go build -ldflags "-X main.commit=${COMMIT}" -o telegabber test: - go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter + go test -v ./config ./ ./telegram ./xmpp ./xmpp/gateway ./persistence ./telegram/formatter ./badger lint: $(GOPATH)/bin/golint ./... diff --git a/README.md b/README.md index 36aa7ca..ae80f4a 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ It is good idea to obtain Telegram API ID from [**https://my.telegram.org**](htt * `--profiling-port=xxxx`: start the pprof server on port `xxxx`. Access is limited to localhost. * `--config=/bla/bla/config.yml`: set the config file path (default: `config.yml`). * `--schema=/bla/bla/schema.json`: set the schema file path (default: `./config_schema.json`). +* `--ids=/bla/bla/ids`: set the folder for ids database (default: `ids`). ### How to receive files from Telegram ### diff --git a/badger/ids.go b/badger/ids.go new file mode 100644 index 0000000..80fb9ad --- /dev/null +++ b/badger/ids.go @@ -0,0 +1,132 @@ +package badger + +import ( + "bytes" + "errors" + "fmt" + "strconv" + + badger "github.com/dgraph-io/badger/v4" + log "github.com/sirupsen/logrus" +) + +// IdsDB represents a Badger database +type IdsDB struct { + db *badger.DB +} + +// IdsDBOpen returns a new DB object +func IdsDBOpen(path string) IdsDB { + bdb, err := badger.Open(badger.DefaultOptions(path)) + if err != nil { + log.Errorf("Failed to open ids database: %v, falling back to in-memory database", path) + bdb, err = badger.Open(badger.DefaultOptions("").WithInMemory(true)) + if err != nil { + log.Fatalf("Couldn't initialize the ids database") + } + } + + return IdsDB{ + db: bdb, + } +} + +// Set stores an id pair +func (db *IdsDB) Set(tgAccount, xmppAccount string, tgChatId, tgMsgId int64, xmppId string) error { + bPrefix := toKeyPrefix(tgAccount, xmppAccount) + bTgId := toTgByteString(tgChatId, tgMsgId) + bXmppId := toXmppByteString(xmppId) + bTgKey := toByteKey(bPrefix, bTgId, "tg") + bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp") + + return db.db.Update(func(txn *badger.Txn) error { + if err := txn.Set(bTgKey, bXmppId); err != nil { + return err + } + return txn.Set(bXmppKey, bTgId) + }) +} + +func (db *IdsDB) getByteValue(key []byte) ([]byte, error) { + var valCopy []byte + err := db.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(key) + if err != nil { + return err + } + + valCopy, err = item.ValueCopy(nil) + return err + }) + return valCopy, err +} + +// GetByTgIds obtains an XMPP id by Telegram chat/message ids +func (db *IdsDB) GetByTgIds(tgAccount, xmppAccount string, tgChatId, tgMsgId int64) (string, error) { + val, err := db.getByteValue(toByteKey( + toKeyPrefix(tgAccount, xmppAccount), + toTgByteString(tgChatId, tgMsgId), + "tg", + )) + if err != nil { + return "", err + } + return string(val), nil +} + +// GetByXmppId obtains Telegram chat/message ids by an XMPP id +func (db *IdsDB) GetByXmppId(tgAccount, xmppAccount, xmppId string) (int64, int64, error) { + val, err := db.getByteValue(toByteKey( + toKeyPrefix(tgAccount, xmppAccount), + toXmppByteString(xmppId), + "xmpp", + )) + if err != nil { + return 0, 0, err + } + return splitTgByteString(val) +} + +func toKeyPrefix(tgAccount, xmppAccount string) []byte { + return []byte(fmt.Sprintf("%v/%v/", tgAccount, xmppAccount)) +} + +func toByteKey(prefix, suffix []byte, typ string) []byte { + key := make([]byte, 0, len(prefix) + len(suffix) + 6) + key = append(key, prefix...) + key = append(key, []byte(typ)...) + key = append(key, []byte("/")...) + key = append(key, suffix...) + return key +} + +func toTgByteString(tgChatId, tgMsgId int64) []byte { + return []byte(fmt.Sprintf("%v/%v", tgChatId, tgMsgId)) +} + +func toXmppByteString(xmppId string) []byte { + return []byte(xmppId) +} + +func splitTgByteString(val []byte) (int64, int64, error) { + parts := bytes.Split(val, []byte("/")) + if len(parts) != 2 { + return 0, 0, errors.New("Couldn't parse tg id pair") + } + tgChatId, err := strconv.ParseInt(string(parts[0]), 10, 64) + if err != nil { + return 0, 0, err + } + tgMsgId, err := strconv.ParseInt(string(parts[1]), 10, 64) + return tgChatId, tgMsgId, err +} + +// Gc compacts the value log +func (db *IdsDB) Gc() { + db.db.RunValueLogGC(0.7) +} + +// Close closes a DB +func (db *IdsDB) Close() { + db.db.Close() +} diff --git a/badger/ids_test.go b/badger/ids_test.go new file mode 100644 index 0000000..efafdeb --- /dev/null +++ b/badger/ids_test.go @@ -0,0 +1,72 @@ +package badger + +import ( + "reflect" + "testing" +) + +func TestToKeyPrefix(t *testing.T) { + if !reflect.DeepEqual(toKeyPrefix("+123456789", "test@example.com"), []byte("+123456789/test@example.com/")) { + t.Error("Wrong prefix") + } +} + +func TestToByteKey(t *testing.T) { + if !reflect.DeepEqual(toByteKey([]byte("ababa/galamaga/"), []byte("123"), "ppp"), []byte("ababa/galamaga/ppp/123")) { + t.Error("Wrong key") + } +} + +func TestToTgByteString(t *testing.T) { + if !reflect.DeepEqual(toTgByteString(-2345, 6789), []byte("-2345/6789")) { + t.Error("Wrong tg string") + } +} + +func TestToXmppByteString(t *testing.T) { + if !reflect.DeepEqual(toXmppByteString("aboba"), []byte("aboba")) { + t.Error("Wrong xmpp string") + } +} + +func TestSplitTgByteStringUnparsable(t *testing.T) { + _, _, err := splitTgByteString([]byte("@#U*&$(@#")) + if err == nil { + t.Error("Unparsable should not be parsed") + return + } + if err.Error() != "Couldn't parse tg id pair" { + t.Error("Wrong parse error") + } +} + +func TestSplitTgByteManyParts(t *testing.T) { + _, _, err := splitTgByteString([]byte("a/b/c/d")) + if err == nil { + t.Error("Should not parse many parts") + return + } + if err.Error() != "Couldn't parse tg id pair" { + t.Error("Wrong parse error") + } +} + +func TestSplitTgByteNonNumeric(t *testing.T) { + _, _, err := splitTgByteString([]byte("0/a")) + if err == nil { + t.Error("Should not parse non-numeric msgid") + } +} + +func TestSplitTgByteSuccess(t *testing.T) { + chatId, msgId, err := splitTgByteString([]byte("-198282398/23798478")) + if err != nil { + t.Error("Should be parsed well") + } + if chatId != -198282398 { + t.Error("Wrong chatId") + } + if msgId != 23798478 { + t.Error("Wrong msgId") + } +} diff --git a/go.mod b/go.mod index 41f4e67..a999878 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.13 require ( github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 - github.com/pkg/errors v0.8.1 + github.com/dgraph-io/badger/v4 v4.1.0 // indirect + github.com/pkg/errors v0.9.1 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.4.2 github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e diff --git a/go.sum b/go.sum index 5fa5f81..0134061 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,13 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829 h1:qe81G6+t1V1ySRMa7lSu5CayN5aP5GEiHXL2DYwHzuA= dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= 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/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 h1:GbV1Lv3lVHsSeKAqPTBem72OCsGjXntW4jfJdXciE+w= github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7/go.mod h1:ZzkRfuaFj8etIYMj/ECtXtgfz72RE6U+dos27b3XIwk= +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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/bodqhrohro/go-tdlib v0.1.1 h1:lmHognymABxP3cmHkfAGhGnWaJaZ3htpJ7RSbZacin4= github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121200156-e826071d3317 h1:+mv4FwWXl8hTa7PrhekwVzPknH+rHqB60jIPBi2XqI8= github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121200156-e826071d3317/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= @@ -20,15 +23,30 @@ github.com/bodqhrohro/go-xmpp v0.2.1-0.20211205194122-f8c4ecb59d8b h1:rTK55SNCBm github.com/bodqhrohro/go-xmpp v0.2.1-0.20211205194122-f8c4ecb59d8b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/bodqhrohro/go-xmpp v0.2.1-0.20211218153313-a8aadd78b65b h1:VDi8z3PzEDhQzazRRuv1fkv662DT3Mm/TY/Lni2Sgrc= github.com/bodqhrohro/go-xmpp v0.2.1-0.20211218153313-a8aadd78b65b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4APOQ= +github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -40,24 +58,45 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godcong/go-tdlib v0.4.4-0.20211203152853-64d22ab8d4ac h1:5FQGW4yHSkbwm+4i/8ef7FvkIFt4NOM4HexSbvPduRo= github.com/godcong/go-tdlib v0.4.4-0.20211203152853-64d22ab8d4ac/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -68,14 +107,20 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -83,8 +128,14 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e h1:Xmvww1DnnCj49ZNwQAw069Kc6X3Q0MUd9OiFQ092yQQ= github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e/go.mod h1:RUak+ZC0a3E2NDNxZmA34Hk4Jm9nJMCSKLpKeB06clM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= @@ -92,43 +143,112 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zelenin/go-tdlib v0.1.0 h1:Qq+FGE0/EWdsRB6m26ULDndu2DtW558aFXNzi0Y/FqQ= github.com/zelenin/go-tdlib v0.1.0/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= github.com/zelenin/go-tdlib v0.5.2 h1:inEATEM0Pz6/HBI3wTlhd+brDHpmoXGgwdSb8/V6GiA= github.com/zelenin/go-tdlib v0.5.2/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -142,6 +262,7 @@ gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 h1:E3uJqX6ImJL9AFdjGbiW04jq8I gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= diff --git a/telegabber.go b/telegabber.go index c8d6a8f..d133d80 100644 --- a/telegabber.go +++ b/telegabber.go @@ -35,6 +35,8 @@ func main() { var configPath = flag.String("config", "config.yml", "Config file path") // JSON schema (not for editing by a user) var schemaPath = flag.String("schema", "./config_schema.json", "Schema file path") + // Folder for Badger DB of message ids + var idsPath = flag.String("ids", "ids", "Ids folder path") var versionFlag = flag.Bool("version", false, "Print the version and exit") flag.Parse() @@ -62,7 +64,7 @@ func main() { log.Infof("Starting telegabber version %v", version) - sm, component, err = xmpp.NewComponent(config.XMPP, config.Telegram) + sm, component, err = xmpp.NewComponent(config.XMPP, config.Telegram, *idsPath) if err != nil { log.Fatal(err) } diff --git a/telegram/commands.go b/telegram/commands.go index ec06f36..59fe1c7 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -498,7 +498,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Last message is empty", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", 0) + content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", "", 0) if content != nil { c.client.EditMessageText(&client.EditMessageTextRequest{ @@ -515,7 +515,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Not enough arguments", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", 0) + content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", "", 0) if content != nil { _, err := c.client.SendMessage(&client.SendMessageRequest{ @@ -594,7 +594,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 1), "", 0) + content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 1), "", "", 0) if content != nil { _, err := c.client.SendMessage(&client.SendMessageRequest{ diff --git a/telegram/utils.go b/telegram/utils.go index 851c6c1..0a2ae1f 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -987,7 +987,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } // ProcessOutgoingMessage executes commands or sends messages to mapped chats -func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64) client.InputMessageContent { +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, id string, replyId int64) client.InputMessageContent { if !c.Online() { // we're offline return nil @@ -1111,7 +1111,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } if chatID != 0 { - _, err := c.client.SendMessage(&client.SendMessageRequest{ + tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ ChatId: chatID, ReplyToMessageId: reply, InputMessageContent: message, @@ -1123,6 +1123,11 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str fmt.Sprintf("Not sent: %s", err.Error()), c.xmpp, ) + } else { + err = gateway.IdsDB.Set(c.Session.Login, c.jid, tgMessage.ChatId, tgMessage.Id, id) + if err != nil { + log.Errorf("Failed to save ids %v/%v %v", tgMessage.ChatId, tgMessage.Id, id) + } } return nil } else { diff --git a/xmpp/component.go b/xmpp/component.go index 0f23d50..f0c481d 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/config" "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/telegram" @@ -38,7 +39,7 @@ var sizeRegex = regexp.MustCompile("\\A([0-9]+) ?([KMGTPE]?B?)\\z") // NewComponent starts a new component and wraps it in // a stream manager that you should start yourself -func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig) (*xmpp.StreamManager, *xmpp.Component, error) { +func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath string) (*xmpp.StreamManager, *xmpp.Component, error) { var err error gateway.Jid, err = stanza.NewJid(conf.Jid) @@ -53,6 +54,8 @@ func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig) (*xmpp.Strea } } + gateway.IdsDB = badger.IdsDBOpen(idsPath) + tgConf = tc if tc.Content.Quota != "" { @@ -163,6 +166,8 @@ func heartbeat(component *xmpp.Component) { // it would be resolved on the next iteration SaveSessions() } + + gateway.IdsDB.Gc() } } @@ -240,6 +245,9 @@ func Close(component *xmpp.Component) { // save sessions SaveSessions() + // flush the ids database + gateway.IdsDB.Close() + // close stream component.Disconnect() } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 534ee7e..29c8a07 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -6,6 +6,7 @@ import ( "strings" "sync" + "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/xmpp/extensions" log "github.com/sirupsen/logrus" @@ -30,6 +31,9 @@ var QueueLock = sync.Mutex{} // Jid stores the component's JID object var Jid *stanza.Jid +// IdsDB provides a disk-backed bidirectional dictionary of Telegram and XMPP ids +var IdsDB badger.IdsDB + // DirtySessions denotes that some Telegram session configurations // were changed and need to be re-flushed to the YamlDB var DirtySessions = false diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 780478a..5178acd 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -139,7 +139,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } } - session.ProcessOutgoingMessage(toID, text, msg.From, replyId) + session.ProcessOutgoingMessage(toID, text, msg.From, msg.Id, replyId) return } else { toJid, err := stanza.NewJid(msg.To) From a5c90340ad2da16a68ddcbfa3d9573f0664067ca Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 3 Jun 2023 20:25:00 -0400 Subject: [PATCH 009/228] Refactor ProcessOutgoingMessage --- telegram/commands.go | 6 +-- telegram/utils.go | 119 +++++++++++++++++++------------------------ 2 files changed, 54 insertions(+), 71 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 59fe1c7..f36108a 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -498,7 +498,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Last message is empty", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", "", 0) + content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) if content != nil { c.client.EditMessageText(&client.EditMessageTextRequest{ @@ -515,7 +515,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Not enough arguments", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", "", 0) + content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) if content != nil { _, err := c.client.SendMessage(&client.SendMessageRequest{ @@ -594,7 +594,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 1), "", "", 0) + content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 1)) if content != nil { _, err := c.client.SendMessage(&client.SendMessageRequest{ diff --git a/telegram/utils.go b/telegram/utils.go index 0a2ae1f..dd63248 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -986,22 +986,27 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } } +// PrepareMessageContent creates a simple text message +func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageContent { + return c.prepareOutgoingMessageContent(text, nil) +} + // ProcessOutgoingMessage executes commands or sends messages to mapped chats -func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, id string, replyId int64) client.InputMessageContent { +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, id string, replyId int64) { if !c.Online() { // we're offline - return nil + return } - if returnJid != "" && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { + if strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!") { // try to execute commands response, isCommand := c.ProcessChatCommand(chatID, text) if response != "" { - gateway.SendTextMessage(returnJid, strconv.FormatInt(chatID, 10), response, c.xmpp) + c.returnMessage(returnJid, chatID, response) } // do not send on success if isCommand { - return nil + return } } @@ -1020,60 +1025,30 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str // attach a file var file *client.InputFileLocal - if chatID != 0 && c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { + if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { response, err := http.Get(text) if err != nil { - gateway.SendTextMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to fetch the uploaded file: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err) } if response != nil && response.Body != nil { defer response.Body.Close() if response.StatusCode != 200 { - gateway.SendTextMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Received status code %v", response.StatusCode), - c.xmpp, - ) - return nil + c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode)) } tempDir, err := ioutil.TempDir("", "telegabber-*") if err != nil { - gateway.SendTextMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to create a temporary directory: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to create a temporary directory", err) } tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text))) if err != nil { - gateway.SendTextMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to create a temporary file: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to create a temporary file", err) } _, err = io.Copy(tempFile, response.Body) if err != nil { - gateway.SendTextMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Failed to write a temporary file: %s", err.Error()), - c.xmpp, - ) - return nil + c.returnError(returnJid, chatID, "Failed to write a temporary file", err) } file = &client.InputFileLocal{ @@ -1092,47 +1067,55 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } } + content := c.prepareOutgoingMessageContent(text, file) + + tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ + ChatId: chatID, + ReplyToMessageId: reply, + InputMessageContent: content, + }) + if err != nil { + gateway.SendTextMessage( + returnJid, + strconv.FormatInt(chatID, 10), + fmt.Sprintf("Not sent: %s", err.Error()), + c.xmpp, + ) + } else { + err = gateway.IdsDB.Set(c.Session.Login, c.jid, tgMessage.ChatId, tgMessage.Id, id) + if err != nil { + log.Errorf("Failed to save ids %v/%v %v", tgMessage.ChatId, tgMessage.Id, id) + } + } +} + +func (c *Client) returnMessage(returnJid string, chatID int64, text string) { + gateway.SendTextMessage(returnJid, strconv.FormatInt(chatID, 10), text, c.xmpp) +} + +func (c *Client) returnError(returnJid string, chatID int64, msg string, err error) { + c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error())) +} + +func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFileLocal) client.InputMessageContent { formattedText := &client.FormattedText{ Text: text, } - var message client.InputMessageContent + var content client.InputMessageContent if file != nil { // we can try to send a document - message = &client.InputMessageDocument{ + content = &client.InputMessageDocument{ Document: file, Caption: formattedText, } } else { // compile our message - message = &client.InputMessageText{ + content = &client.InputMessageText{ Text: formattedText, } } - - if chatID != 0 { - tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ - ChatId: chatID, - ReplyToMessageId: reply, - InputMessageContent: message, - }) - if err != nil { - gateway.SendTextMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Not sent: %s", err.Error()), - c.xmpp, - ) - } else { - err = gateway.IdsDB.Set(c.Session.Login, c.jid, tgMessage.ChatId, tgMessage.Id, id) - if err != nil { - log.Errorf("Failed to save ids %v/%v %v", tgMessage.ChatId, tgMessage.Id, id) - } - } - return nil - } else { - return message - } + return content } // StatusesRange proxies the following function from unexported cache From 7215d11d7973b9896c6223938649c75165fa3ae7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 5 Jun 2023 04:22:13 -0400 Subject: [PATCH 010/228] XEP-0308 message editing --- badger/ids.go | 98 +++++++++++++++++++++++++++++++++++ telegram/handlers.go | 5 ++ telegram/utils.go | 39 ++++++++------ xmpp/extensions/extensions.go | 17 ++++++ xmpp/handlers.go | 43 ++++++++++++++- 5 files changed, 184 insertions(+), 18 deletions(-) diff --git a/badger/ids.go b/badger/ids.go index 80fb9ad..079887a 100644 --- a/badger/ids.go +++ b/badger/ids.go @@ -121,6 +121,104 @@ func splitTgByteString(val []byte) (int64, int64, error) { return tgChatId, tgMsgId, err } +// ReplaceIdPair replaces an old entry by XMPP ID with both new XMPP and Tg ID +func (db *IdsDB) ReplaceIdPair(tgAccount, xmppAccount, oldXmppId, newXmppId string, newMsgId int64) error { + // read old pair + chatId, oldMsgId, err := db.GetByXmppId(tgAccount, xmppAccount, oldXmppId) + if err != nil { + return err + } + + bPrefix := toKeyPrefix(tgAccount, xmppAccount) + + bOldTgId := toTgByteString(chatId, oldMsgId) + bOldXmppId := toXmppByteString(oldXmppId) + bOldTgKey := toByteKey(bPrefix, bOldTgId, "tg") + bOldXmppKey := toByteKey(bPrefix, bOldXmppId, "xmpp") + + bTgId := toTgByteString(chatId, newMsgId) + bXmppId := toXmppByteString(newXmppId) + bTgKey := toByteKey(bPrefix, bTgId, "tg") + bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp") + + return db.db.Update(func(txn *badger.Txn) error { + // save new pair + if err := txn.Set(bTgKey, bXmppId); err != nil { + return err + } + if err := txn.Set(bXmppKey, bTgId); err != nil { + return err + } + // delete old pair + if err := txn.Delete(bOldTgKey); err != nil { + return err + } + return txn.Delete(bOldXmppKey) + }) +} + +// ReplaceXmppId replaces an old XMPP ID with new XMPP ID and keeps Tg ID intact +func (db *IdsDB) ReplaceXmppId(tgAccount, xmppAccount, oldXmppId, newXmppId string) error { + // read old Tg IDs + chatId, msgId, err := db.GetByXmppId(tgAccount, xmppAccount, oldXmppId) + if err != nil { + return err + } + + bPrefix := toKeyPrefix(tgAccount, xmppAccount) + + bOldXmppId := toXmppByteString(oldXmppId) + bOldXmppKey := toByteKey(bPrefix, bOldXmppId, "xmpp") + + bTgId := toTgByteString(chatId, msgId) + bXmppId := toXmppByteString(newXmppId) + bTgKey := toByteKey(bPrefix, bTgId, "tg") + bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp") + + return db.db.Update(func(txn *badger.Txn) error { + // save new pair + if err := txn.Set(bTgKey, bXmppId); err != nil { + return err + } + if err := txn.Set(bXmppKey, bTgId); err != nil { + return err + } + // delete old xmpp->tg entry + return txn.Delete(bOldXmppKey) + }) +} + +// ReplaceTgId replaces an old Tg ID with new Tg ID and keeps Tg chat ID and XMPP ID intact +func (db *IdsDB) ReplaceTgId(tgAccount, xmppAccount string, chatId, oldMsgId, newMsgId int64) error { + // read old XMPP ID + xmppId, err := db.GetByTgIds(tgAccount, xmppAccount, chatId, oldMsgId) + if err != nil { + return err + } + + bPrefix := toKeyPrefix(tgAccount, xmppAccount) + + bOldTgId := toTgByteString(chatId, oldMsgId) + bOldTgKey := toByteKey(bPrefix, bOldTgId, "tg") + + bTgId := toTgByteString(chatId, newMsgId) + bXmppId := toXmppByteString(xmppId) + bTgKey := toByteKey(bPrefix, bTgId, "tg") + bXmppKey := toByteKey(bPrefix, bXmppId, "xmpp") + + return db.db.Update(func(txn *badger.Txn) error { + // save new pair + if err := txn.Set(bTgKey, bXmppId); err != nil { + return err + } + if err := txn.Set(bXmppKey, bTgId); err != nil { + return err + } + // delete old tg->xmpp entry + return txn.Delete(bOldTgKey) + }) +} + // Gc compacts the value log func (db *IdsDB) Gc() { db.db.RunValueLogGC(0.7) diff --git a/telegram/handlers.go b/telegram/handlers.go index bd768ae..6de59e7 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -272,6 +272,11 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat // clean uploaded files func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { + 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.Error("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) + } + file, _ := c.contentToFile(update.Message.Content) if file != nil && file.Local != nil { c.cleanTempFile(file.Local.Path) diff --git a/telegram/utils.go b/telegram/utils.go index dd63248..ecce6da 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -991,14 +991,14 @@ func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageC return c.prepareOutgoingMessageContent(text, nil) } -// ProcessOutgoingMessage executes commands or sends messages to mapped chats -func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, id string, replyId int64) { +// 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 { if !c.Online() { // we're offline - return + return 0 } - if strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!") { + if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { // try to execute commands response, isCommand := c.ProcessChatCommand(chatID, text) if response != "" { @@ -1006,7 +1006,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } // do not send on success if isCommand { - return + return 0 } } @@ -1014,7 +1014,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str // quotations var reply int64 - if replyId == 0 { + if replaceId == 0 && replyId == 0 { replySlice := replyRegex.FindStringSubmatch(text) if len(replySlice) > 1 { reply, _ = strconv.ParseInt(replySlice[1], 10, 64) @@ -1069,24 +1069,29 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str content := c.prepareOutgoingMessageContent(text, file) + if replaceId != 0 { + tgMessage, err := c.client.EditMessageText(&client.EditMessageTextRequest{ + ChatId: chatID, + MessageId: replaceId, + InputMessageContent: content, + }) + if err != nil { + c.returnError(returnJid, chatID, "Not edited", err) + return 0 + } + return tgMessage.Id + } + tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ ChatId: chatID, ReplyToMessageId: reply, InputMessageContent: content, }) if err != nil { - gateway.SendTextMessage( - returnJid, - strconv.FormatInt(chatID, 10), - fmt.Sprintf("Not sent: %s", err.Error()), - c.xmpp, - ) - } else { - err = gateway.IdsDB.Set(c.Session.Login, c.jid, tgMessage.ChatId, tgMessage.Id, id) - if err != nil { - log.Errorf("Failed to save ids %v/%v %v", tgMessage.ChatId, tgMessage.Id, id) - } + c.returnError(returnJid, chatID, "Not sent", err) + return 0 } + return tgMessage.Id } func (c *Client) returnMessage(returnJid string, chatID int64, text string) { diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 78de47d..2d547af 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -180,6 +180,12 @@ type ClientMessage struct { Extensions []stanza.MsgExtension `xml:",omitempty"` } +// Replace is from XEP-0308 +type Replace struct { + XMLName xml.Name `xml:"urn:xmpp:message-correct:0 replace"` + Id string `xml:"id,attr"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -225,6 +231,11 @@ func (c ComponentPrivilege) Namespace() string { return c.XMLName.Space } +// Namespace is a namespace! +func (c Replace) Namespace() string { + return c.XMLName.Space +} + // Name is a packet name func (ClientMessage) Name() string { return "message" @@ -291,4 +302,10 @@ func init() { "urn:xmpp:privilege:1", "privilege", }, ComponentPrivilege{}) + + // message edit + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:message-correct:0", + "replace", + }, Replace{}) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 5178acd..51fd831 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -102,10 +102,13 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { if ok { var reply extensions.Reply var fallback extensions.Fallback + var replace extensions.Replace msg.Get(&reply) msg.Get(&fallback) + msg.Get(&replace) log.Debugf("reply: %#v", reply) log.Debugf("fallback: %#v", fallback) + log.Debugf("replace: %#v", replace) var replyId int64 var err error @@ -138,8 +141,46 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { text = text[:start] + text[end:] } } + var replaceId int64 + if replace.Id != "" { + chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, replace.Id) + if err == nil { + if chatId != toID { + gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "", component) + return + } + replaceId = msgId + log.Debugf("replace tg: %#v %#v", chatId, msgId) + } else { + gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "", component) + return + } + } - session.ProcessOutgoingMessage(toID, text, msg.From, msg.Id, replyId) + tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) + if tgMessageId != 0 { + if replaceId != 0 { + // not needed (is it persistent among clients though?) + /* err = gateway.IdsDB.ReplaceIdPair(session.Session.Login, bare, replace.Id, msg.Id, tgMessageId) + if err != nil { + log.Errorf("Failed to replace id %v with %v %v", replace.Id, msg.Id, tgMessageId) + } */ + } else { + err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) + if err != nil { + log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) + } + } + } else { + /* + // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway + if replaceId != 0 { + err = gateway.IdsDB.ReplaceXmppId(session.Session.Login, bare, replace.Id, msg.Id) + if err != nil { + log.Errorf("Failed to replace id %v with %v", replace.Id, msg.Id) + } + } */ + } return } else { toJid, err := stanza.NewJid(msg.To) From 945b9c063b8bcf20f0df370fd1f68457f84f3361 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 8 Jun 2023 13:14:55 -0400 Subject: [PATCH 011/228] Reply own messages --- xmpp/handlers.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 51fd831..5034551 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -114,13 +114,23 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { var err error text := msg.Body if len(reply.Id) > 0 { - id := reply.Id - if id[0] == 'e' { - id = id[1:] - } - replyId, err = strconv.ParseInt(id, 10, 64) - if err != nil { - log.Warn(errors.Wrap(err, "Failed to parse message ID!")) + chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, reply.Id) + if err == nil { + if chatId != toID { + log.Warnf("Chat mismatch: %v ≠ %v", chatId, toID) + } else { + replyId = msgId + log.Debugf("replace tg: %#v %#v", chatId, msgId) + } + } else { + id := reply.Id + if id[0] == 'e' { + id = id[1:] + } + replyId, err = strconv.ParseInt(id, 10, 64) + if err != nil { + log.Warn(errors.Wrap(err, "Failed to parse message ID!")) + } } if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 { From 79fc0ddbe5010f54707441cb3f15c43192124ed2 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 8 Jun 2023 13:27:40 -0400 Subject: [PATCH 012/228] Set origin id, if available, to replies bridged from Telegram --- telegram/utils.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index ecce6da..a4a2bd4 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -330,9 +330,13 @@ func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, return } + replyId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, message.ChatId, message.ReplyToMessageId) + if err != nil { + replyId = strconv.FormatInt(message.ReplyToMessageId, 10) + } reply = &gateway.Reply{ Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), - Id: strconv.FormatInt(message.ReplyToMessageId, 10), + Id: replyId, } } From 00f1417cb2ed8199633a12989e9befb53298d407 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 8 Jun 2023 13:33:22 -0400 Subject: [PATCH 013/228] Version 1.6.0 --- badger/ids.go | 2 +- telegabber.go | 2 +- telegram/commands.go | 2 +- telegram/handlers.go | 2 +- telegram/utils.go | 12 ++++++------ xmpp/handlers.go | 16 ++++++++-------- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/badger/ids.go b/badger/ids.go index 079887a..1295e8a 100644 --- a/badger/ids.go +++ b/badger/ids.go @@ -92,7 +92,7 @@ func toKeyPrefix(tgAccount, xmppAccount string) []byte { } func toByteKey(prefix, suffix []byte, typ string) []byte { - key := make([]byte, 0, len(prefix) + len(suffix) + 6) + key := make([]byte, 0, len(prefix)+len(suffix)+6) key = append(key, prefix...) key = append(key, []byte(typ)...) key = append(key, []byte("/")...) diff --git a/telegabber.go b/telegabber.go index d133d80..48db544 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.6.0-dev" +var version string = "1.6.0" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index f36108a..5e16a6a 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -652,7 +652,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) keyValueString("Chat title", info.Fn), keyValueString("Photo", link), keyValueString("Username", info.Nickname), - keyValueString("Full name", info.Given + " " + info.Family), + keyValueString("Full name", info.Given+" "+info.Family), keyValueString("Phone number", info.Tel), } return strings.Join(entries, "\n"), true diff --git a/telegram/handlers.go b/telegram/handlers.go index 6de59e7..57402ab 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -274,7 +274,7 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { 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.Error("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) + log.Errorf("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) } file, _ := c.contentToFile(update.Message.Content) diff --git a/telegram/utils.go b/telegram/utils.go index a4a2bd4..58f7712 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -25,13 +25,13 @@ import ( ) type VCardInfo struct { - Fn string - Photo *client.File + Fn string + Photo *client.File Nickname string - Given string - Family string - Tel string - Info string + Given string + Family string + Tel string + Info string } var errOffline = errors.New("TDlib instance is offline") diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 5034551..d5666b1 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -183,13 +183,13 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } } else { /* - // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway - if replaceId != 0 { - err = gateway.IdsDB.ReplaceXmppId(session.Session.Login, bare, replace.Id, msg.Id) - if err != nil { - log.Errorf("Failed to replace id %v with %v", replace.Id, msg.Id) - } - } */ + // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway + if replaceId != 0 { + err = gateway.IdsDB.ReplaceXmppId(session.Session.Login, bare, replace.Id, msg.Id) + if err != nil { + log.Errorf("Failed to replace id %v with %v", replace.Id, msg.Id) + } + } */ } return } else { @@ -225,7 +225,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { suffix := "@" + msg.From for bare := range sessions { if strings.HasSuffix(bare, suffix) { - gateway.SendServiceMessage(bare, "Your server \"" + msg.From + "\" does not allow to send carbons", component) + gateway.SendServiceMessage(bare, "Your server \""+msg.From+"\" does not allow to send carbons", component) } } } From edf2c08a5b46e48277a5173653cfe8b156a412e5 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 11 Jun 2023 13:53:36 -0400 Subject: [PATCH 014/228] Bump Go version to 1.19 --- go.mod | 25 ++++++++++++++-- go.sum | 83 +++++++-------------------------------------------- telegabber.go | 2 +- 3 files changed, 33 insertions(+), 77 deletions(-) diff --git a/go.mod b/go.mod index a999878..a4e26d2 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module dev.narayana.im/narayana/telegabber -go 1.13 +go 1.19 require ( - github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 - github.com/dgraph-io/badger/v4 v4.1.0 // indirect + github.com/dgraph-io/badger/v4 v4.1.0 github.com/pkg/errors v0.9.1 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.4.2 @@ -14,4 +13,24 @@ require ( gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 ) +require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + 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 + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + nhooyr.io/websocket v1.6.5 // indirect +) + replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f diff --git a/go.sum b/go.sum index 0134061..011bc93 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829 h1:qe81G6+t1V1ySRMa7lSu5CayN5aP5GEiHXL2DYwHzuA= -dev.narayana.im/narayana/go-xmpp v0.0.0-20211218155535-e55463fc9829/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= 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/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7 h1:GbV1Lv3lVHsSeKAqPTBem72OCsGjXntW4jfJdXciE+w= -github.com/Arman92/go-tdlib v0.0.0-20191002071913-526f4e1d15f7/go.mod h1:ZzkRfuaFj8etIYMj/ECtXtgfz72RE6U+dos27b3XIwk= 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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/bodqhrohro/go-tdlib v0.1.1 h1:lmHognymABxP3cmHkfAGhGnWaJaZ3htpJ7RSbZacin4= -github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121200156-e826071d3317 h1:+mv4FwWXl8hTa7PrhekwVzPknH+rHqB60jIPBi2XqI8= -github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121200156-e826071d3317/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= -github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121233100-48d2382034fb h1:y5PnjdAnNVS0q8xuwjm3TxBfLriJmykQdoGiyYZB3s0= -github.com/bodqhrohro/go-tdlib v0.1.2-0.20191121233100-48d2382034fb/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= -github.com/bodqhrohro/go-tdlib v0.4.4-0.20211229000346-ee6018be8ec0 h1:9ysLk2hG2q0NeNdX6StzS+4fTAG2FeZJYKKegCuB4q4= -github.com/bodqhrohro/go-tdlib v0.4.4-0.20211229000346-ee6018be8ec0/go.mod h1:sOdXFpJ3zn6RHRc8aNVkJYALHpoplwBgMwIbRCYABIg= -github.com/bodqhrohro/go-xmpp v0.1.4-0.20191106203535-f3b463f3b26c h1:LzcQyE+Gs+0kAbpnPAUD68FvUCieKZip44URAmH70PI= -github.com/bodqhrohro/go-xmpp v0.1.4-0.20191106203535-f3b463f3b26c/go.mod h1:fWixaMaFvx8cxXcJVJ5kU9csMeD/JN8on7ybassU8rY= -github.com/bodqhrohro/go-xmpp v0.2.1-0.20191105232737-9abd5be0aa1b h1:9BLd/SNO4JJZLRl1Qb1v9mNivIlHuwHDe2c8hQvBxFA= -github.com/bodqhrohro/go-xmpp v0.2.1-0.20191105232737-9abd5be0aa1b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= -github.com/bodqhrohro/go-xmpp v0.2.1-0.20211205194122-f8c4ecb59d8b h1:rTK55SNCBmssyRgNAweVwVVfuoRstI8RbL+8Ys/RzxE= -github.com/bodqhrohro/go-xmpp v0.2.1-0.20211205194122-f8c4ecb59d8b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= -github.com/bodqhrohro/go-xmpp v0.2.1-0.20211218153313-a8aadd78b65b h1:VDi8z3PzEDhQzazRRuv1fkv662DT3Mm/TY/Lni2Sgrc= -github.com/bodqhrohro/go-xmpp v0.2.1-0.20211218153313-a8aadd78b65b/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -33,10 +13,6 @@ github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVz github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -44,6 +20,7 @@ github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4 github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -56,8 +33,6 @@ github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/godcong/go-tdlib v0.4.4-0.20211203152853-64d22ab8d4ac h1:5FQGW4yHSkbwm+4i/8ef7FvkIFt4NOM4HexSbvPduRo= -github.com/godcong/go-tdlib v0.4.4-0.20211203152853-64d22ab8d4ac/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= @@ -74,17 +49,14 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -92,11 +64,13 @@ github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/Vq github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -107,20 +81,15 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -128,41 +97,30 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e h1:Xmvww1DnnCj49ZNwQAw069Kc6X3Q0MUd9OiFQ092yQQ= github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e/go.mod h1:RUak+ZC0a3E2NDNxZmA34Hk4Jm9nJMCSKLpKeB06clM= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zelenin/go-tdlib v0.1.0 h1:Qq+FGE0/EWdsRB6m26ULDndu2DtW558aFXNzi0Y/FqQ= -github.com/zelenin/go-tdlib v0.1.0/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= github.com/zelenin/go-tdlib v0.5.2 h1:inEATEM0Pz6/HBI3wTlhd+brDHpmoXGgwdSb8/V6GiA= github.com/zelenin/go-tdlib v0.5.2/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -170,7 +128,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -181,8 +138,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -192,37 +147,25 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -232,8 +175,6 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -248,7 +189,7 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -256,10 +197,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gosrc.io/xmpp v0.1.3 h1:VYP1bA35irlQ1ZAJqNhJOz8NSsSTkzQRhREfmuG1H80= -gosrc.io/xmpp v0.1.3/go.mod h1:fWixaMaFvx8cxXcJVJ5kU9csMeD/JN8on7ybassU8rY= -gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 h1:E3uJqX6ImJL9AFdjGbiW04jq8IQ+NcOK+JSiWq2TbRw= -gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/telegabber.go b/telegabber.go index 48db544..78b893b 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.6.0" +var version string = "1.6.1-dev" var commit string var sm *goxmpp.StreamManager From fdd867cf7a435346a552a8da7f446b59b5e1213e Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 16 Jun 2023 00:34:49 -0400 Subject: [PATCH 015/228] Add /cancelauth command --- telegram/commands.go | 10 +++++++++- telegram/connect.go | 30 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 5e16a6a..cafeb0a 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -44,6 +44,7 @@ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ "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"}, @@ -230,7 +231,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string switch cmd { case "login", "code", "password": if cmd == "login" && c.Session.Login != "" { - return "" + return "Phone number already provided, use /cancelauth to start over" } if len(args) < 1 { @@ -286,6 +287,13 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } c.Session.Login = "" + // cancel auth + case "cancelauth": + if c.Online() { + return "Not allowed when online" + } + c.cancelAuth() + return "Cancelled" // set @username case "setusername": if !c.Online() { diff --git a/telegram/connect.go b/telegram/connect.go index 2633980..8324319 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -24,6 +24,9 @@ type clientAuthorizer struct { } func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.AuthorizationState) error { + if stateHandler.isClosed { + return errors.New("Channel is closed") + } stateHandler.State <- state switch state.AuthorizationStateType() { @@ -84,6 +87,9 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth } func (stateHandler *clientAuthorizer) Close() { + if stateHandler.isClosed { + return + } stateHandler.isClosed = true close(stateHandler.TdlibParameters) close(stateHandler.PhoneNumber) @@ -191,11 +197,7 @@ func (c *Client) Disconnect(resource string, quit bool) bool { ) } - _, err := c.client.Close() - if err != nil { - log.Errorf("Couldn't close the Telegram instance: %v; %#v", err, c) - } - c.forceClose() + c.close() return true } @@ -242,6 +244,24 @@ func (c *Client) forceClose() { c.authorizer = nil } +func (c *Client) close() { + if c.authorizer != nil && !c.authorizer.isClosed { + c.authorizer.Close() + } + if c.client != nil { + _, err := c.client.Close() + if err != nil { + log.Errorf("Couldn't close the Telegram instance: %v; %#v", err, c) + } + } + c.forceClose() +} + +func (c *Client) cancelAuth() { + c.close() + c.Session.Login = "" +} + // Online checks if the updates listener is alive func (c *Client) Online() bool { return c.online From 739fc4110a9c12aa51cbd6361e5c4398c8ed7ff8 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 16 Jun 2023 00:44:48 -0400 Subject: [PATCH 016/228] Fix a crash by auth commands when online --- telegram/commands.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telegram/commands.go b/telegram/commands.go index cafeb0a..2f879b1 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -259,6 +259,10 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string return telegramNotInitialized } + if c.authorizer.isClosed { + return "Authorization is done already" + } + switch cmd { // sign in case "login": @@ -290,7 +294,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string // cancel auth case "cancelauth": if c.Online() { - return "Not allowed when online" + return "Not allowed when online, use /logout instead" } c.cancelAuth() return "Cancelled" From f8ad8c0204e2effabce4545c91992146a560168d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 30 Jun 2023 08:48:36 -0400 Subject: [PATCH 017/228] Update chat title in chats cache --- telegram/handlers.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 57402ab..65e5f2f 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -294,8 +294,13 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) // set also the status (for group chats only) - _, user, _ := c.GetContactByID(update.ChatId, nil) + chat, user, _ := c.GetContactByID(update.ChatId, nil) if user == nil { c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true)) } + + // update chat title in the cache + if chat != nil { + chat.Title = update.Title + } } From 30b3fd16153fab727315f014150d98b367ccd6ad Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 30 Jun 2023 09:54:39 -0400 Subject: [PATCH 018/228] Force update nicknames via PubSub and presences on reconnect --- telegram/utils.go | 15 +++++++++++++++ xmpp/handlers.go | 1 + 2 files changed, 16 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index 58f7712..a9e0bc8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1332,3 +1332,18 @@ func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { return info, nil } + +func (c *Client) UpdateChatNicknames() { + for _, id := range c.cache.ChatsKeys() { + chat, ok := c.cache.GetChat(id) + if ok { + gateway.SendPresence( + c.xmpp, + c.jid, + gateway.SPFrom(strconv.FormatInt(id, 10)), + gateway.SPNickname(chat.Title), + ) + gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) + } + } +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index d5666b1..1286914 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -342,6 +342,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { gateway.SPImmed(false), ) } + session.UpdateChatNicknames() } }() } From 959dc061ff30ba1cf5c699adc0f7d1d991d7afa5 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 8 Jul 2023 23:52:30 -0400 Subject: [PATCH 019/228] Send carbons for outgoing messages to other resources --- telegram/client.go | 6 +++- telegram/commands.go | 7 ++-- telegram/handlers.go | 28 +++++++++------- telegram/utils.go | 74 +++++++++++++++++++++++++++++------------ xmpp/gateway/gateway.go | 14 ++++---- xmpp/handlers.go | 3 ++ 6 files changed, 89 insertions(+), 43 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 71d8125..61d46aa 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -52,6 +52,7 @@ type Client struct { jid string Session *persistence.Session resources map[string]bool + outbox map[string]string content *config.TelegramContentConfig cache *cache.Cache online bool @@ -59,13 +60,15 @@ type Client struct { DelayedStatuses map[int64]*DelayedStatus DelayedStatusesLock sync.Mutex - locks clientLocks + locks clientLocks + SendMessageLock sync.Mutex } type clientLocks struct { authorizationReady sync.Mutex chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex + outboxLock sync.Mutex } // NewClient instantiates a Telegram App @@ -121,6 +124,7 @@ 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(), options: options, diff --git a/telegram/commands.go b/telegram/commands.go index 2f879b1..53b5d75 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -513,11 +513,14 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) if content != nil { - c.client.EditMessageText(&client.EditMessageTextRequest{ + _, err = c.client.EditMessageText(&client.EditMessageTextRequest{ ChatId: chatID, MessageId: message.Id, InputMessageContent: content, }) + if err != nil { + return "Message editing error", true + } } else { return "Message processing error", true } @@ -650,7 +653,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } if messages != nil && messages.Messages != nil { for _, message := range messages.Messages { - c.ProcessIncomingMessage(targetChatId, message) + c.ProcessIncomingMessage(targetChatId, message, "") } } // print vCard diff --git a/telegram/handlers.go b/telegram/handlers.go index 65e5f2f..8173ecd 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -203,26 +203,30 @@ func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) { // message received func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { - go func() { - chatId := update.Message.ChatId + chatId := update.Message.ChatId - // guarantee sequential message delivering per chat - lock := c.getChatMessageLock(chatId) + c.SendMessageLock.Lock() + c.SendMessageLock.Unlock() + xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, update.Message.Id) + var ignoredResource string + if err == nil { + ignoredResource = c.popFromOutbox(xmppId) + } else { + log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.Message.Id) + } + log.Warnf("xmppId: %v, ignoredResource: %v", xmppId, ignoredResource) + + // guarantee sequential message delivering per chat + lock := c.getChatMessageLock(chatId) + go func() { lock.Lock() defer lock.Unlock() - // ignore self outgoing messages - if update.Message.IsOutgoing && - update.Message.SendingState != nil && - update.Message.SendingState.MessageSendingStateType() == client.TypeMessageSendingStatePending { - return - } - log.WithFields(log.Fields{ "chat_id": chatId, }).Warn("New message from chat") - c.ProcessIncomingMessage(chatId, update.Message) + c.ProcessIncomingMessage(chatId, update.Message, ignoredResource) }() } diff --git a/telegram/utils.go b/telegram/utils.go index a9e0bc8..9664857 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -890,9 +890,34 @@ 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) { - var text, oob, auxText string +func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message, ignoredResource string) { + var jids []string + var isPM bool var err error + if gateway.MessageOutgoingPermission && c.Session.Carbons { + isPM, err = c.IsPM(chatId) + if err != nil { + log.Errorf("Could not determine if chat is PM: %v", err) + } + } + isOutgoing := message.IsOutgoing + isCarbon := isPM && isOutgoing + log.Warnf("isOutgoing: %v", isOutgoing) + if isOutgoing { + for resource := range c.resourcesRange() { + if ignoredResource == "" || resource != ignoredResource { + jids = append(jids, c.jid+"/"+resource) + } + } + if len(jids) == 0 { + log.Info("The only resource is ignored, aborting") + return + } + } else { + jids = []string{c.jid} + } + + var text, oob, auxText string reply, replyMsg := c.getMessageReply(message) @@ -965,27 +990,10 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { sId := strconv.FormatInt(message.Id, 10) sChatId := strconv.FormatInt(chatId, 10) - var jids []string - var isPM bool - if gateway.MessageOutgoingPermission && c.Session.Carbons { - isPM, err = c.IsPM(chatId) - if err != nil { - log.Errorf("Could not determine if chat is PM: %v", err) - } - } - isOutgoing := isPM && message.IsOutgoing - if isOutgoing { - for resource := range c.resourcesRange() { - jids = append(jids, c.jid+"/"+resource) - } - } else { - jids = []string{c.jid} - } - for _, jid := range jids { - gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isOutgoing) + gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon) if auxText != "" { - gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isOutgoing) + gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon) } } } @@ -1172,9 +1180,12 @@ func (c *Client) resourcesRange() chan string { // resend statuses to (to another resource, for example) func (c *Client) roster(resource string) { + c.locks.resourcesLock.Lock() if _, ok := c.resources[resource]; ok { + c.locks.resourcesLock.Unlock() return // we know it } + c.locks.resourcesLock.Unlock() log.Warnf("Sending roster for %v", resource) @@ -1347,3 +1358,24 @@ func (c *Client) UpdateChatNicknames() { } } } + +// AddToOutbox remembers the resource from which a message with given ID was sent +func (c *Client) AddToOutbox(xmppId, resource string) { + c.locks.outboxLock.Lock() + defer c.locks.outboxLock.Unlock() + + c.outbox[xmppId] = resource +} + +func (c *Client) popFromOutbox(xmppId string) string { + c.locks.outboxLock.Lock() + defer c.locks.outboxLock.Unlock() + + resource, ok := c.outbox[xmppId] + if ok { + delete(c.outbox, xmppId) + } else { + log.Warnf("No %v xmppId in outbox", xmppId) + } + return resource +} diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 29c8a07..7e54ee5 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -42,8 +42,8 @@ var DirtySessions = false var MessageOutgoingPermission = false // SendMessage creates and sends a message stanza -func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isOutgoing bool) { - sendMessageWrapper(to, from, body, id, component, reply, "", isOutgoing) +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) } // SendServiceMessage creates and sends a simple message stanza from transport @@ -57,11 +57,11 @@ func SendTextMessage(to string, from string, body string, component *xmpp.Compon } // 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, isOutgoing bool) { - sendMessageWrapper(to, from, body, id, component, reply, oob, isOutgoing) +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) } -func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isOutgoing bool) { +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -83,7 +83,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen logFrom = from messageFrom = from + "@" + componentJid } - if isOutgoing { + if isCarbon { messageTo = messageFrom messageFrom = bareTo + "/" + Jid.Resource } else { @@ -120,7 +120,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen } } - if isOutgoing { + if isCarbon { carbonMessage := extensions.ClientMessage{ Attrs: stanza.Attrs{ From: bareTo, diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1286914..e6671bc 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -167,6 +167,8 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } } + session.SendMessageLock.Lock() + defer session.SendMessageLock.Unlock() tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) if tgMessageId != 0 { if replaceId != 0 { @@ -181,6 +183,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) } } + session.AddToOutbox(msg.Id, resource) } else { /* // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway From e954c73bd2d881bc448b6bb2b3cb3a4ad37d0139 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 15 Jul 2023 21:38:10 -0400 Subject: [PATCH 020/228] Do not ack with edited message to the XEP-0308 sender resource --- telegram/handlers.go | 23 +++++++++++++++++++++-- telegram/utils.go | 33 +++++++++++++++++++-------------- xmpp/handlers.go | 3 ++- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 8173ecd..abc1f5d 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -214,7 +214,6 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { } else { log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.Message.Id) } - log.Warnf("xmppId: %v, ignoredResource: %v", xmppId, ignoredResource) // guarantee sequential message delivering per chat lock := c.getChatMessageLock(chatId) @@ -233,6 +232,24 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { // message content updated func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { markupFunction := formatter.EntityToXEP0393 + + c.SendMessageLock.Lock() + c.SendMessageLock.Unlock() + xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId) + var ignoredResource string + if err == nil { + ignoredResource = c.popFromOutbox(xmppId) + } else { + log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.MessageId) + } + log.Infof("ignoredResource: %v", ignoredResource) + + jids := c.getCarbonFullJids(true, ignoredResource) + if len(jids) == 0 { + log.Info("The only resource is ignored, aborting") + return + } + if update.NewContent.MessageContentType() == client.TypeMessageText { textContent := update.NewContent.(*client.MessageText) var editChar string @@ -246,7 +263,9 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { textContent.Text.Entities, markupFunction, )) - gateway.SendMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false) + for _, jid := range jids { + gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, false) + } } } diff --git a/telegram/utils.go b/telegram/utils.go index 9664857..9486349 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -891,7 +891,6 @@ 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, ignoredResource string) { - var jids []string var isPM bool var err error if gateway.MessageOutgoingPermission && c.Session.Carbons { @@ -900,21 +899,13 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message, i log.Errorf("Could not determine if chat is PM: %v", err) } } + isOutgoing := message.IsOutgoing isCarbon := isPM && isOutgoing - log.Warnf("isOutgoing: %v", isOutgoing) - if isOutgoing { - for resource := range c.resourcesRange() { - if ignoredResource == "" || resource != ignoredResource { - jids = append(jids, c.jid+"/"+resource) - } - } - if len(jids) == 0 { - log.Info("The only resource is ignored, aborting") - return - } - } else { - jids = []string{c.jid} + jids := c.getCarbonFullJids(isOutgoing, ignoredResource) + if len(jids) == 0 { + log.Info("The only resource is ignored, aborting") + return } var text, oob, auxText string @@ -1379,3 +1370,17 @@ func (c *Client) popFromOutbox(xmppId string) string { } return resource } + +func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []string { + var jids []string + if isOutgoing { + for resource := range c.resourcesRange() { + if ignoredResource == "" || resource != ignoredResource { + jids = append(jids, c.jid+"/"+resource) + } + } + } else { + jids = []string{c.jid} + } + return jids +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index e6671bc..c5ec029 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -177,13 +177,14 @@ 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) } else { err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) if err != nil { log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) } + session.AddToOutbox(msg.Id, resource) } - session.AddToOutbox(msg.Id, resource) } else { /* // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway From 563cb2d624598efdd3819daef00c64079d8a20e1 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 16 Jul 2023 08:19:11 -0400 Subject: [PATCH 021/228] Avoid webpage preview updates being sent as message edits --- telegram/client.go | 3 +++ telegram/handlers.go | 20 ++++++++++++++++++++ telegram/utils.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/telegram/client.go b/telegram/client.go index 61d46aa..5cc15a4 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -53,6 +53,7 @@ type Client struct { Session *persistence.Session resources map[string]bool outbox map[string]string + editQueue map[ChatMessageId]bool content *config.TelegramContentConfig cache *cache.Cache online bool @@ -69,6 +70,7 @@ type clientLocks struct { chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex outboxLock sync.Mutex + editQueueLock sync.Mutex } // NewClient instantiates a Telegram App @@ -125,6 +127,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component Session: session, resources: make(map[string]bool), outbox: make(map[string]string), + editQueue: make(map[ChatMessageId]bool), content: &conf.Content, cache: cache.NewCache(), options: options, diff --git a/telegram/handlers.go b/telegram/handlers.go index abc1f5d..93ac284 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -104,6 +104,13 @@ func (c *Client) updateHandler() { } c.updateNewMessage(typedUpdate) log.Debugf("%#v", typedUpdate.Message) + case client.TypeUpdateMessageEdited: + typedUpdate, ok := update.(*client.UpdateMessageEdited) + if !ok { + uhOh() + } + c.updateMessageEdited(typedUpdate) + log.Debugf("%#v", typedUpdate) case client.TypeUpdateMessageContent: typedUpdate, ok := update.(*client.UpdateMessageContent) if !ok { @@ -229,6 +236,11 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { }() } +// message content edited +func (c *Client) updateMessageEdited(update *client.UpdateMessageEdited) { + c.addToEditQueue(update.ChatId, update.MessageId) +} + // message content updated func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { markupFunction := formatter.EntityToXEP0393 @@ -244,6 +256,14 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } log.Infof("ignoredResource: %v", ignoredResource) + if !c.deleteFromEditQueue(update.ChatId, update.MessageId) { + log.WithFields(log.Fields{ + "chatId": update.ChatId, + "messageId": update.MessageId, + }).Infof("Content update with no preceding message edit, ignoring") + return + } + jids := c.getCarbonFullJids(true, ignoredResource) if len(jids) == 0 { log.Info("The only resource is ignored, aborting") diff --git a/telegram/utils.go b/telegram/utils.go index 9486349..da66189 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -24,6 +24,7 @@ import ( "github.com/zelenin/go-tdlib/client" ) +// VCardInfo contains intermediate data to produce a vCard type VCardInfo struct { Fn string Photo *client.File @@ -34,6 +35,12 @@ type VCardInfo struct { Info string } +// ChatMessageId uniquely identifies a Telegram message +type ChatMessageId struct { + ChatId int64 + MessageId int64 +} + var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) @@ -1384,3 +1391,29 @@ func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []st } return jids } + +func (c *Client) addToEditQueue(chatId, messageId int64) { + c.locks.editQueueLock.Lock() + defer c.locks.editQueueLock.Unlock() + + c.editQueue[ChatMessageId{ + ChatId: chatId, + MessageId: messageId, + }] = true +} + +func (c *Client) deleteFromEditQueue(chatId, messageId int64) bool { + c.locks.editQueueLock.Lock() + defer c.locks.editQueueLock.Unlock() + + key := ChatMessageId{ + ChatId: chatId, + MessageId: messageId, + } + _, ok := c.editQueue[key] + if ok { + delete(c.editQueue, key) + } + + return ok +} From eadef987be11dc22a89c2aad990814bd89add770 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 21 Jul 2023 07:45:44 -0400 Subject: [PATCH 022/228] Revert "Avoid webpage preview updates being sent as message edits" This reverts commit 563cb2d624598efdd3819daef00c64079d8a20e1. --- telegram/client.go | 3 --- telegram/handlers.go | 20 -------------------- telegram/utils.go | 33 --------------------------------- 3 files changed, 56 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 5cc15a4..61d46aa 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -53,7 +53,6 @@ type Client struct { Session *persistence.Session resources map[string]bool outbox map[string]string - editQueue map[ChatMessageId]bool content *config.TelegramContentConfig cache *cache.Cache online bool @@ -70,7 +69,6 @@ type clientLocks struct { chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex outboxLock sync.Mutex - editQueueLock sync.Mutex } // NewClient instantiates a Telegram App @@ -127,7 +125,6 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component Session: session, resources: make(map[string]bool), outbox: make(map[string]string), - editQueue: make(map[ChatMessageId]bool), content: &conf.Content, cache: cache.NewCache(), options: options, diff --git a/telegram/handlers.go b/telegram/handlers.go index 93ac284..abc1f5d 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -104,13 +104,6 @@ func (c *Client) updateHandler() { } c.updateNewMessage(typedUpdate) log.Debugf("%#v", typedUpdate.Message) - case client.TypeUpdateMessageEdited: - typedUpdate, ok := update.(*client.UpdateMessageEdited) - if !ok { - uhOh() - } - c.updateMessageEdited(typedUpdate) - log.Debugf("%#v", typedUpdate) case client.TypeUpdateMessageContent: typedUpdate, ok := update.(*client.UpdateMessageContent) if !ok { @@ -236,11 +229,6 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { }() } -// message content edited -func (c *Client) updateMessageEdited(update *client.UpdateMessageEdited) { - c.addToEditQueue(update.ChatId, update.MessageId) -} - // message content updated func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { markupFunction := formatter.EntityToXEP0393 @@ -256,14 +244,6 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } log.Infof("ignoredResource: %v", ignoredResource) - if !c.deleteFromEditQueue(update.ChatId, update.MessageId) { - log.WithFields(log.Fields{ - "chatId": update.ChatId, - "messageId": update.MessageId, - }).Infof("Content update with no preceding message edit, ignoring") - return - } - jids := c.getCarbonFullJids(true, ignoredResource) if len(jids) == 0 { log.Info("The only resource is ignored, aborting") diff --git a/telegram/utils.go b/telegram/utils.go index da66189..9486349 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -24,7 +24,6 @@ import ( "github.com/zelenin/go-tdlib/client" ) -// VCardInfo contains intermediate data to produce a vCard type VCardInfo struct { Fn string Photo *client.File @@ -35,12 +34,6 @@ type VCardInfo struct { Info string } -// ChatMessageId uniquely identifies a Telegram message -type ChatMessageId struct { - ChatId int64 - MessageId int64 -} - var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) @@ -1391,29 +1384,3 @@ func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []st } return jids } - -func (c *Client) addToEditQueue(chatId, messageId int64) { - c.locks.editQueueLock.Lock() - defer c.locks.editQueueLock.Unlock() - - c.editQueue[ChatMessageId{ - ChatId: chatId, - MessageId: messageId, - }] = true -} - -func (c *Client) deleteFromEditQueue(chatId, messageId int64) bool { - c.locks.editQueueLock.Lock() - defer c.locks.editQueueLock.Unlock() - - key := ChatMessageId{ - ChatId: chatId, - MessageId: messageId, - } - _, ok := c.editQueue[key] - if ok { - delete(c.editQueue, key) - } - - return ok -} From 748366ad6a9dc4b2a269d5499ae1d5d7e8526762 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 22 Jul 2023 10:46:35 -0400 Subject: [PATCH 023/228] Avoid webpage preview updates being sent as message edits (by hash matching) --- telegram/client.go | 7 ++++++ telegram/handlers.go | 6 ++++- telegram/utils.go | 57 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/telegram/client.go b/telegram/client.go index 61d46aa..5b0217a 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -2,6 +2,7 @@ package telegram import ( "github.com/pkg/errors" + "hash/maphash" "path/filepath" "strconv" "sync" @@ -60,6 +61,9 @@ type Client struct { DelayedStatuses map[int64]*DelayedStatus DelayedStatusesLock sync.Mutex + lastMsgHashes map[int64]uint64 + msgHashSeed maphash.Seed + locks clientLocks SendMessageLock sync.Mutex } @@ -69,6 +73,7 @@ type clientLocks struct { chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex outboxLock sync.Mutex + lastMsgHashesLock sync.Mutex } // NewClient instantiates a Telegram App @@ -129,6 +134,8 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component cache: cache.NewCache(), options: options, DelayedStatuses: make(map[int64]*DelayedStatus), + lastMsgHashes: make(map[int64]uint64), + msgHashSeed: maphash.MakeSeed(), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), }, diff --git a/telegram/handlers.go b/telegram/handlers.go index abc1f5d..cc6e635 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -226,6 +226,8 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { }).Warn("New message from chat") c.ProcessIncomingMessage(chatId, update.Message, ignoredResource) + + c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) }() } @@ -233,6 +235,8 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { markupFunction := formatter.EntityToXEP0393 + defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent) + c.SendMessageLock.Lock() c.SendMessageLock.Unlock() xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, update.ChatId, update.MessageId) @@ -250,7 +254,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { return } - if update.NewContent.MessageContentType() == client.TypeMessageText { + 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 { diff --git a/telegram/utils.go b/telegram/utils.go index 9486349..4835696 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2,8 +2,10 @@ package telegram import ( "crypto/sha1" + "encoding/binary" "fmt" "github.com/pkg/errors" + "hash/maphash" "io" "io/ioutil" "net/http" @@ -1384,3 +1386,58 @@ func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []st } return jids } + +func (c *Client) calculateMessageHash(messageId int64, content client.MessageContent) uint64 { + var h maphash.Hash + h.SetSeed(c.msgHashSeed) + + buf8 := make([]byte, 8) + binary.BigEndian.PutUint64(buf8, uint64(messageId)) + h.Write(buf8) + + if content != nil && content.MessageContentType() == client.TypeMessageText { + textContent, ok := content.(*client.MessageText) + if !ok { + uhOh() + } + + if textContent.Text != nil { + h.WriteString(textContent.Text.Text) + for _, entity := range textContent.Text.Entities { + buf4 := make([]byte, 4) + binary.BigEndian.PutUint32(buf4, uint32(entity.Offset)) + h.Write(buf4) + binary.BigEndian.PutUint32(buf4, uint32(entity.Length)) + h.Write(buf4) + h.WriteString(entity.Type.TextEntityTypeType()) + } + } + } + + return h.Sum64() +} + +func (c *Client) updateLastMessageHash(chatId, messageId int64, content client.MessageContent) { + c.locks.lastMsgHashesLock.Lock() + defer c.locks.lastMsgHashesLock.Unlock() + + c.lastMsgHashes[chatId] = c.calculateMessageHash(messageId, content) +} + +func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content client.MessageContent) bool { + c.locks.lastMsgHashesLock.Lock() + defer c.locks.lastMsgHashesLock.Unlock() + + oldHash, ok := c.lastMsgHashes[chatId] + newHash := c.calculateMessageHash(messageId, content) + + if !ok { + log.Warnf("Last message hash for chat %v does not exist", chatId) + } + log.WithFields(log.Fields{ + "old hash": oldHash, + "new hash": newHash, + }).Info("Message hashes") + + return !ok || oldHash != newHash +} From ef831fc9725601a94149ce94c3fb686afc77e0a5 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 31 Jul 2023 21:25:24 -0400 Subject: [PATCH 024/228] Migrate to TDLib 1.8.14 (multiple usernames support) --- go.mod | 1 + go.sum | 2 ++ telegram/client.go | 4 +-- telegram/commands.go | 26 +++++++++----- telegram/connect.go | 12 ++----- telegram/handlers.go | 2 +- telegram/utils.go | 80 ++++++++++++++++++++++++++++++++---------- telegram/utils_test.go | 47 +++++++++++++++++++++++++ xmpp/handlers.go | 8 ++--- 9 files changed, 138 insertions(+), 44 deletions(-) diff --git a/go.mod b/go.mod index a4e26d2..db7c380 100644 --- a/go.mod +++ b/go.mod @@ -34,3 +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 diff --git a/go.sum b/go.sum index 011bc93..2565582 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ 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-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/client.go b/telegram/client.go index 5b0217a..e9acd20 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -45,7 +45,7 @@ type DelayedStatus struct { type Client struct { client *client.Client authorizer *clientAuthorizer - parameters *client.TdlibParameters + parameters *client.SetTdlibParametersRequest options []client.Option me *client.User @@ -100,7 +100,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component datadir = "./sessions/" // ye olde defaute } - parameters := client.TdlibParameters{ + parameters := client.SetTdlibParametersRequest{ UseTestDc: false, DatabaseDirectory: filepath.Join(datadir, jid), diff --git a/telegram/commands.go b/telegram/commands.go index 53b5d75..3828ec2 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -18,8 +18,7 @@ const notEnoughArguments string = "Not enough arguments" const telegramNotInitialized string = "Telegram connection is not initialized yet" const notOnline string = "Not online" -var permissionsAdmin = client.ChatMemberStatusAdministrator{ - CanBeEdited: true, +var permissionsAdmin = client.ChatAdministratorRights{ CanChangeInfo: true, CanPostMessages: true, CanEditMessages: true, @@ -30,14 +29,20 @@ var permissionsAdmin = client.ChatMemberStatusAdministrator{ CanPromoteMembers: false, } var permissionsMember = client.ChatPermissions{ - CanSendMessages: true, - CanSendMediaMessages: true, + CanSendBasicMessages: true, + CanSendAudios: true, + CanSendDocuments: true, + CanSendPhotos: true, + CanSendVideos: true, + CanSendVideoNotes: true, + CanSendVoiceNotes: true, CanSendPolls: true, CanSendOtherMessages: true, CanAddWebPagePreviews: true, CanChangeInfo: true, CanInviteUsers: true, CanPinMessages: true, + CanManageTopics: true, } var permissionsReadonly = client.ChatPermissions{} @@ -666,7 +671,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) entries := []string{ keyValueString("Chat title", info.Fn), keyValueString("Photo", link), - keyValueString("Username", info.Nickname), + keyValueString("Usernames", c.usernamesToString(info.Nicknames)), keyValueString("Full name", info.Given+" "+info.Family), keyValueString("Phone number", info.Tel), } @@ -883,7 +888,10 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } // clone the permissions - status := permissionsAdmin + status := client.ChatMemberStatusAdministrator{ + CanBeEdited: true, + Rights: &permissionsAdmin, + } if len(args) > 1 { status.CustomTitle = args[1] @@ -933,9 +941,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Invalid TTL", true } } - _, err = c.client.SetChatMessageTtl(&client.SetChatMessageTtlRequest{ - ChatId: chatID, - Ttl: int32(ttl), + _, err = c.client.SetChatMessageAutoDeleteTime(&client.SetChatMessageAutoDeleteTimeRequest{ + ChatId: chatID, + MessageAutoDeleteTime: int32(ttl), }) if err != nil { diff --git a/telegram/connect.go b/telegram/connect.go index 8324319..ef03428 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -13,7 +13,7 @@ import ( const chatsLimit int32 = 999 type clientAuthorizer struct { - TdlibParameters chan *client.TdlibParameters + TdlibParameters chan *client.SetTdlibParametersRequest PhoneNumber chan string Code chan string State chan client.AuthorizationState @@ -31,13 +31,7 @@ func (stateHandler *clientAuthorizer) Handle(c *client.Client, state client.Auth switch state.AuthorizationStateType() { case client.TypeAuthorizationStateWaitTdlibParameters: - _, err := c.SetTdlibParameters(&client.SetTdlibParametersRequest{ - Parameters: <-stateHandler.TdlibParameters, - }) - return err - - case client.TypeAuthorizationStateWaitEncryptionKey: - _, err := c.CheckDatabaseEncryptionKey(&client.CheckDatabaseEncryptionKeyRequest{}) + _, err := c.SetTdlibParameters(<-stateHandler.TdlibParameters) return err case client.TypeAuthorizationStateWaitPhoneNumber: @@ -116,7 +110,7 @@ func (c *Client) Connect(resource string) error { log.Warn("Connecting to Telegram network...") c.authorizer = &clientAuthorizer{ - TdlibParameters: make(chan *client.TdlibParameters, 1), + TdlibParameters: make(chan *client.SetTdlibParametersRequest, 1), PhoneNumber: make(chan string, 1), Code: make(chan string, 1), State: make(chan client.AuthorizationState, 10), diff --git a/telegram/handlers.go b/telegram/handlers.go index cc6e635..cedea63 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -233,7 +233,7 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { // message content updated func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { - markupFunction := formatter.EntityToXEP0393 + markupFunction := c.getFormatter() defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent) diff --git a/telegram/utils.go b/telegram/utils.go index 4835696..e1e9317 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -27,13 +27,13 @@ import ( ) type VCardInfo struct { - Fn string - Photo *client.File - Nickname string - Given string - Family string - Tel string - Info string + Fn string + Photo *client.File + Nicknames []string + Given string + Family string + Tel string + Info string } var errOffline = errors.New("TDlib instance is offline") @@ -286,12 +286,15 @@ func (c *Client) formatContact(chatID int64) string { if chat != nil { str = fmt.Sprintf("%s (%v)", chat.Title, chat.Id) } else if user != nil { - username := user.Username - if username == "" { - username = strconv.FormatInt(user.Id, 10) + var usernames string + if user.Usernames != nil { + usernames = c.usernamesToString(user.Usernames.ActiveUsernames) + } + if usernames == "" { + usernames = strconv.FormatInt(user.Id, 10) } - str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, username) + str = fmt.Sprintf("%s %s (%v)", user.FirstName, user.LastName, usernames) } else { str = strconv.FormatInt(chatID, 10) } @@ -566,7 +569,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return "" } - markupFunction := formatter.EntityToXEP0393 + markupFunction := c.getFormatter() switch message.Content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := message.Content.(*client.MessageSticker) @@ -737,6 +740,22 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return strings.Join(rows, "\n") } + case client.TypeMessageChatSetMessageAutoDeleteTime: + ttl, _ := message.Content.(*client.MessageChatSetMessageAutoDeleteTime) + name := c.formatContact(ttl.FromUserId) + if name == "" { + if ttl.MessageAutoDeleteTime == 0 { + return "The self-destruct timer was disabled" + } else { + return fmt.Sprintf("The self-destruct timer was set to %v seconds", ttl.MessageAutoDeleteTime) + } + } else { + if ttl.MessageAutoDeleteTime == 0 { + return fmt.Sprintf("%s disabled the self-destruct timer", name) + } else { + return fmt.Sprintf("%s set the self-destruct timer to %v seconds", name, ttl.MessageAutoDeleteTime) + } + } } return fmt.Sprintf("unknown message (%s)", message.Content.MessageContentType()) @@ -751,7 +770,7 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl case client.TypeMessageSticker: sticker, _ := content.(*client.MessageSticker) file := sticker.Sticker.Sticker - if sticker.Sticker.IsAnimated && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { + if sticker.Sticker.Format.StickerFormatType() != client.TypeStickerTypeRegular && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { file = sticker.Sticker.Thumbnail.File } return file, nil @@ -1192,7 +1211,7 @@ func (c *Client) roster(resource string) { } // get last messages from specified chat -func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.Messages, error) { +func (c *Client) getLastMessages(id int64, query string, from int64, count int32) (*client.FoundChatMessages, error) { return c.client.SearchChatMessages(&client.SearchChatMessagesRequest{ ChatId: id, Query: query, @@ -1245,10 +1264,18 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { UserId: privateType.UserId, }) if err == nil { - if fullInfo.Bio != "" { - return fullInfo.Bio - } else if fullInfo.Description != "" { - return fullInfo.Description + if fullInfo.Bio != nil && fullInfo.Bio.Text != "" { + return formatter.Format( + fullInfo.Bio.Text, + fullInfo.Bio.Entities, + c.getFormatter(), + ) + } else if fullInfo.BotInfo != nil { + if fullInfo.BotInfo.ShortDescription != "" { + return fullInfo.BotInfo.ShortDescription + } else { + return fullInfo.BotInfo.Description + } } } else { log.Warnf("Coudln't retrieve private chat info: %v", err.Error()) @@ -1328,7 +1355,10 @@ func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { info.Info = c.GetChatDescription(chat) } if user != nil { - info.Nickname = user.Username + if user.Usernames != nil { + info.Nicknames = make([]string, len(user.Usernames.ActiveUsernames)) + copy(info.Nicknames, user.Usernames.ActiveUsernames) + } info.Given = user.FirstName info.Family = user.LastName info.Tel = user.PhoneNumber @@ -1441,3 +1471,15 @@ 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) usernamesToString(usernames []string) string { + var atUsernames []string + for _, username := range usernames { + atUsernames = append(atUsernames, "@"+username) + } + return strings.Join(atUsernames, ", ") +} diff --git a/telegram/utils_test.go b/telegram/utils_test.go index 91002ee..18be215 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -369,6 +369,53 @@ func TestMessageAnimation(t *testing.T) { } } +func TestMessageTtl1(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{}, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "The self-destruct timer was disabled" { + t.Errorf("Wrong anonymous off ttl label: %v", text) + } +} + +func TestMessageTtl2(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{ + MessageAutoDeleteTime: 3, + }, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "The self-destruct timer was set to 3 seconds" { + t.Errorf("Wrong anonymous ttl label: %v", text) + } +} + +func TestMessageTtl3(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{ + FromUserId: 3, + }, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "unknown contact: TDlib instance is offline disabled the self-destruct timer" { + t.Errorf("Wrong off ttl label: %v", text) + } +} + +func TestMessageTtl4(t *testing.T) { + ttl := client.Message{ + Content: &client.MessageChatSetMessageAutoDeleteTime{ + FromUserId: 3, + MessageAutoDeleteTime: 3, + }, + } + text := (&Client{}).messageToText(&ttl, false) + if text != "unknown contact: TDlib instance is offline set the self-destruct timer to 3 seconds" { + t.Errorf("Wrong ttl label: %v", text) + } +} + func TestMessageUnknown(t *testing.T) { unknown := client.Message{ Content: &client.MessageExpiredPhoto{}, diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c5ec029..fd1afad 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -481,7 +481,7 @@ func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *tel vcard.Photo.Type.Text = "image/jpeg" vcard.Photo.Binval.Text = base64Photo } - vcard.Nickname.Text = info.Nickname + vcard.Nickname.Text = strings.Join(info.Nicknames, ",") vcard.N.Given.Text = info.Given vcard.N.Family.Text = info.Family vcard.Tel.Number.Text = info.Tel @@ -512,13 +512,13 @@ func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *tel }, }) } - if info.Nickname != "" { + for _, nickname := range info.Nicknames { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "nickname"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: info.Nickname, + Content: nickname, }, }, }, stanza.Node{ @@ -526,7 +526,7 @@ func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *tel Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, - Content: "https://t.me/" + info.Nickname, + Content: "https://t.me/" + nickname, }, }, }) From a595d9db0af3e5c08abf4df32de61e653af0711c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 31 Jul 2023 21:37:05 -0400 Subject: [PATCH 025/228] Version 1.7.0 --- telegabber.go | 2 +- telegram/commands.go | 4 ++-- telegram/utils_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/telegabber.go b/telegabber.go index 78b893b..5bb69d6 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.6.1-dev" +var version string = "1.7.0" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 3828ec2..e164ce1 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -942,8 +942,8 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } } _, err = c.client.SetChatMessageAutoDeleteTime(&client.SetChatMessageAutoDeleteTimeRequest{ - ChatId: chatID, - MessageAutoDeleteTime: int32(ttl), + ChatId: chatID, + MessageAutoDeleteTime: int32(ttl), }) if err != nil { diff --git a/telegram/utils_test.go b/telegram/utils_test.go index 18be215..e54ddb5 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -406,7 +406,7 @@ func TestMessageTtl3(t *testing.T) { func TestMessageTtl4(t *testing.T) { ttl := client.Message{ Content: &client.MessageChatSetMessageAutoDeleteTime{ - FromUserId: 3, + FromUserId: 3, MessageAutoDeleteTime: 3, }, } From 131f6eba38734212e039845b6cd9deabbc9d978d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 31 Jul 2023 22:00:58 -0400 Subject: [PATCH 026/228] Use previews only instead of TGS stickers --- telegabber.go | 2 +- telegram/utils.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/telegabber.go b/telegabber.go index 5bb69d6..cfeecda 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.7.0" +var version string = "1.7.1" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index e1e9317..cd25c22 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -770,7 +770,7 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl case client.TypeMessageSticker: sticker, _ := content.(*client.MessageSticker) file := sticker.Sticker.Sticker - if sticker.Sticker.Format.StickerFormatType() != client.TypeStickerTypeRegular && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { + if sticker.Sticker.Format.StickerFormatType() == client.TypeStickerFormatTgs && sticker.Sticker.Thumbnail != nil && sticker.Sticker.Thumbnail.File != nil { file = sticker.Sticker.Thumbnail.File } return file, nil From a5f6c600357d4000b4a904e36cac1256ca3ac01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C4=BCja=20Pav=C4=BCikhin?= Date: Tue, 1 Aug 2023 22:36:27 +0000 Subject: [PATCH 027/228] Add building in fcking docker environment --- .gitignore | 1 + Dockerfile | 36 ++++++++++++++++++++++++++++++++++++ Makefile | 6 ++++++ 3 files changed, 43 insertions(+) create mode 100644 Dockerfile diff --git a/.gitignore b/.gitignore index 58426dd..b132b72 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ telegabber sessions/ session.dat session.dat.new +release/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6fea570 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM golang:1.19-bookworm AS base + +RUN apt-get update +run apt-get install -y libssl-dev cmake build-essential gperf libz-dev make git + +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 . ${MAKEOPTS} +RUN make install + +FROM base AS cache +ARG VERSION +COPY --from=tdlib /compiled/ /usr/local/ +COPY ./ /src +RUN git -C /src checkout "${VERSION}" +WORKDIR /src +RUN go get + +FROM cache AS build +ARG MAKEOPTS +WORKDIR /src +RUN make ${MAKEOPTS} + +FROM scratch AS telegabber +COPY --from=build /src/telegabber /usr/local/bin/ +ENTRYPOINT ["/usr/local/bin/telegabber"] + +FROM scratch AS binaries +COPY --from=telegabber /usr/local/bin/telegabber / diff --git a/Makefile b/Makefile index 048987d..5869f9e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ .PHONY: all test COMMIT := $(shell git rev-parse --short HEAD) +TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" +VERSION := "v1.7.1" +MAKEOPTS := "-j4" all: go build -ldflags "-X main.commit=${COMMIT}" -o telegabber @@ -10,3 +13,6 @@ test: lint: $(GOPATH)/bin/golint ./... + +build_indocker: + docker build --build-arg "TD_COMMIT=${TD_COMMIT}" --build-arg "VERSION=${VERSION}" --build-arg "MAKEOPTS=${MAKEOPTS}" --output=release --target binaries . From 8fc9edd7e70aeca266ab2860198de49bdc2ab585 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 1 Aug 2023 20:03:34 -0400 Subject: [PATCH 028/228] Prevent messages to a certain resource from being carbon-copied --- Makefile | 2 +- telegabber.go | 2 +- xmpp/gateway/gateway.go | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5869f9e..2eb17ac 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.7.1" +VERSION := "v1.7.2" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index cfeecda..9ce070f 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.7.1" +var version string = "1.7.2" var commit string var sm *goxmpp.StreamManager diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 7e54ee5..1be2fca 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -119,6 +119,9 @@ func sendMessageWrapper(to string, from string, body string, id string, componen message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) } } + if !isCarbon && toJid.Resource != "" { + message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) + } if isCarbon { carbonMessage := extensions.ClientMessage{ From 3c917c16983c1afdd4a21d8021461585a1e785c9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 2 Aug 2023 13:53:34 -0400 Subject: [PATCH 029/228] Carbons in group chats --- telegabber.go | 2 +- telegram/utils.go | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/telegabber.go b/telegabber.go index 9ce070f..671e13b 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.7.2" +var version string = "1.8.0-dev" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index cd25c22..a91c6da 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -837,12 +837,15 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, if err != nil { log.Errorf("Could not determine if chat is PM: %v", err) } + isCarbonsEnabled := gateway.MessageOutgoingPermission && c.Session.Carbons + // 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 - if !isPM || !gateway.MessageOutgoingPermission || !c.Session.Carbons { + if !hideSender { if c.Session.AsciiArrows { if message.IsOutgoing { directionChar = "> " @@ -861,7 +864,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) } // show sender in group chats - if !isPM { + if !hideSender { sender := c.formatSender(message) if sender != "" { prefix = append(prefix, sender) @@ -912,17 +915,12 @@ 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, ignoredResource string) { - var isPM bool - var err error + var isCarbon bool + isOutgoing := message.IsOutgoing if gateway.MessageOutgoingPermission && c.Session.Carbons { - isPM, err = c.IsPM(chatId) - if err != nil { - log.Errorf("Could not determine if chat is PM: %v", err) - } + isCarbon = isOutgoing } - isOutgoing := message.IsOutgoing - isCarbon := isPM && isOutgoing jids := c.getCarbonFullJids(isOutgoing, ignoredResource) if len(jids) == 0 { log.Info("The only resource is ignored, aborting") From 608f67551297a14e2e23603413bbce66f6ad5cd9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 2 Aug 2023 16:41:18 -0400 Subject: [PATCH 030/228] Revert sending carbons for outgoing messages to other resources (they duplicate what clients already send to each other) --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 2 +- telegram/handlers.go | 19 ++++++++----------- telegram/utils.go | 11 +++-------- xmpp/handlers.go | 1 - 6 files changed, 14 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 2eb17ac..6732740 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.7.2" +VERSION := "v1.7.3" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 9ce070f..3d7d2ea 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.7.2" +var version string = "1.7.3" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index e164ce1..206e049 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -658,7 +658,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } if messages != nil && messages.Messages != nil { for _, message := range messages.Messages { - c.ProcessIncomingMessage(targetChatId, message, "") + c.ProcessIncomingMessage(targetChatId, message) } } // print vCard diff --git a/telegram/handlers.go b/telegram/handlers.go index cedea63..0d1cda9 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -205,27 +205,24 @@ func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) { func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { chatId := update.Message.ChatId - c.SendMessageLock.Lock() - c.SendMessageLock.Unlock() - xmppId, err := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, update.Message.Id) - var ignoredResource string - if err == nil { - ignoredResource = c.popFromOutbox(xmppId) - } else { - log.Infof("Couldn't retrieve XMPP message ids for %v, an echo may happen", update.Message.Id) - } - // guarantee sequential message delivering per chat lock := c.getChatMessageLock(chatId) go func() { lock.Lock() defer lock.Unlock() + // ignore self outgoing messages + if update.Message.IsOutgoing && + update.Message.SendingState != nil && + update.Message.SendingState.MessageSendingStateType() == client.TypeMessageSendingStatePending { + return + } + log.WithFields(log.Fields{ "chat_id": chatId, }).Warn("New message from chat") - c.ProcessIncomingMessage(chatId, update.Message, ignoredResource) + c.ProcessIncomingMessage(chatId, update.Message) c.updateLastMessageHash(update.Message.ChatId, update.Message.Id, update.Message.Content) }() diff --git a/telegram/utils.go b/telegram/utils.go index cd25c22..62ce945 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -911,7 +911,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, ignoredResource string) { +func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { var isPM bool var err error if gateway.MessageOutgoingPermission && c.Session.Carbons { @@ -921,13 +921,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message, i } } - isOutgoing := message.IsOutgoing - isCarbon := isPM && isOutgoing - jids := c.getCarbonFullJids(isOutgoing, ignoredResource) - if len(jids) == 0 { - log.Info("The only resource is ignored, aborting") - return - } + isCarbon := isPM && message.IsOutgoing + jids := c.getCarbonFullJids(isCarbon, "") var text, oob, auxText string diff --git a/xmpp/handlers.go b/xmpp/handlers.go index fd1afad..4e3354e 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -183,7 +183,6 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { if err != nil { log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) } - session.AddToOutbox(msg.Id, resource) } } else { /* From c03ccfdfb713d4fcb089600d9fd91f03e469daca Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 2 Aug 2023 17:08:06 -0400 Subject: [PATCH 031/228] Support urn:xmpp:privilege:2 --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 2 +- telegram/utils.go | 4 ++-- xmpp/extensions/extensions.go | 26 ++++++++++++++++++++++---- xmpp/gateway/gateway.go | 22 +++++++++++++++------- xmpp/handlers.go | 21 ++++++++++++++++----- 7 files changed, 58 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 6732740..7857e17 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.7.3" +VERSION := "v1.7.4" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 3d7d2ea..df13dd6 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.7.3" +var version string = "1.7.4" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 206e049..0c83945 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -384,7 +384,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } case "config": if len(args) > 1 { - if !gateway.MessageOutgoingPermission && args[0] == "carbons" && args[1] == "true" { + if gateway.MessageOutgoingPermissionVersion == 0 && args[0] == "carbons" && args[1] == "true" { return "The server did not allow to enable carbons" } diff --git a/telegram/utils.go b/telegram/utils.go index 62ce945..4caf88c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -842,7 +842,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, prefix := []string{} // message direction var directionChar string - if !isPM || !gateway.MessageOutgoingPermission || !c.Session.Carbons { + if !isPM || gateway.MessageOutgoingPermissionVersion == 0 || !c.Session.Carbons { if c.Session.AsciiArrows { if message.IsOutgoing { directionChar = "> " @@ -914,7 +914,7 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File { func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { var isPM bool var err error - if gateway.MessageOutgoingPermission && c.Session.Carbons { + if gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons { isPM, err = c.IsPM(chatId) if err != nil { log.Errorf("Could not determine if chat is PM: %v", err) diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 2d547af..192b630 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -154,12 +154,19 @@ type CarbonSent struct { } // ComponentPrivilege is from XEP-0356 -type ComponentPrivilege struct { +type ComponentPrivilege1 struct { XMLName xml.Name `xml:"urn:xmpp:privilege:1 privilege"` Perms []ComponentPerm `xml:"perm"` Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` } +// ComponentPrivilege is from XEP-0356 +type ComponentPrivilege2 struct { + XMLName xml.Name `xml:"urn:xmpp:privilege:2 privilege"` + Perms []ComponentPerm `xml:"perm"` + Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` +} + // ComponentPerm is from XEP-0356 type ComponentPerm struct { XMLName xml.Name `xml:"perm"` @@ -227,7 +234,12 @@ func (c CarbonSent) Namespace() string { } // Namespace is a namespace! -func (c ComponentPrivilege) Namespace() string { +func (c ComponentPrivilege1) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c ComponentPrivilege2) Namespace() string { return c.XMLName.Space } @@ -297,11 +309,17 @@ func init() { "sent", }, CarbonSent{}) - // component privilege + // component privilege v1 stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ "urn:xmpp:privilege:1", "privilege", - }, ComponentPrivilege{}) + }, ComponentPrivilege1{}) + + // component privilege v2 + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:privilege:2", + "privilege", + }, ComponentPrivilege2{}) // message edit stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 1be2fca..7a2500e 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -38,8 +38,8 @@ var IdsDB badger.IdsDB // were changed and need to be re-flushed to the YamlDB var DirtySessions = false -// MessageOutgoingPermission allows to fake outgoing messages by foreign JIDs -var MessageOutgoingPermission = false +// MessageOutgoingPermissionVersion contains a XEP-0356 version to fake outgoing messages by foreign JIDs +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) { @@ -142,11 +142,19 @@ func sendMessageWrapper(to string, from string, body string, id string, componen To: toJid.Domain, }, } - privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege{ - Forwarded: stanza.Forwarded{ - Stanza: carbonMessage, - }, - }) + if MessageOutgoingPermissionVersion == 2 { + privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege2{ + Forwarded: stanza.Forwarded{ + Stanza: carbonMessage, + }, + }) + } else { + privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege1{ + Forwarded: stanza.Forwarded{ + Stanza: carbonMessage, + }, + }) + } sendMessage(&privilegeMessage, component) } else { sendMessage(&message, component) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 4e3354e..6679a72 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -209,14 +209,25 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } if msg.Body == "" { - var privilege extensions.ComponentPrivilege - if ok := msg.Get(&privilege); ok { - log.Debugf("privilege: %#v", privilege) + var privilege1 extensions.ComponentPrivilege1 + if ok := msg.Get(&privilege1); ok { + log.Debugf("privilege1: %#v", privilege1) } - for _, perm := range privilege.Perms { + for _, perm := range privilege1.Perms { if perm.Access == "message" && perm.Type == "outgoing" { - gateway.MessageOutgoingPermission = true + gateway.MessageOutgoingPermissionVersion = 1 + } + } + + var privilege2 extensions.ComponentPrivilege2 + if ok := msg.Get(&privilege2); ok { + log.Debugf("privilege2: %#v", privilege2) + } + + for _, perm := range privilege2.Perms { + if perm.Access == "message" && perm.Type == "outgoing" { + gateway.MessageOutgoingPermissionVersion = 2 } } } From 9377d7a15538a6c0af97937806ecd55eb112beb3 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 6 Aug 2023 20:04:49 -0400 Subject: [PATCH 032/228] Save/read unavailable presence type in cache --- Makefile | 2 +- telegabber.go | 2 +- telegram/cache/cache.go | 10 ++++++++++ telegram/utils.go | 40 ++++++++++++++++++++++++++++++++++++---- xmpp/handlers.go | 14 +++++++++++--- 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 7857e17..33bedad 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.7.4" +VERSION := "v1.7.5" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index df13dd6..37d5890 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.7.4" +var version string = "1.7.5" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/cache/cache.go b/telegram/cache/cache.go index 3d9608d..6847d3e 100644 --- a/telegram/cache/cache.go +++ b/telegram/cache/cache.go @@ -133,3 +133,13 @@ func (cache *Cache) SetStatus(id int64, show string, status string) { Description: status, } } + +// Destruct splits a cached status into show, description and type +func (status *Status) Destruct() (show, description, typ string) { + show, description = status.XMPP, status.Description + if show == "unavailable" { + typ = show + show = "" + } + return +} diff --git a/telegram/utils.go b/telegram/utils.go index 4caf88c..2578671 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -243,15 +243,33 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o cachedStatus, ok := c.cache.GetStatus(chatID) if status == "" { if ok { - show, status = cachedStatus.XMPP, cachedStatus.Description + var typ string + show, status, typ = cachedStatus.Destruct() + if presenceType == "" { + presenceType = typ + } + log.WithFields(log.Fields{ + "show": show, + "status": status, + "presenceType": presenceType, + }).Debug("Cached status") } else if user != nil && user.Status != nil { show, status, presenceType = c.userStatusToText(user.Status, chatID) + log.WithFields(log.Fields{ + "show": show, + "status": status, + "presenceType": presenceType, + }).Debug("Status to text") } else { show, status = "chat", chat.Title } } - c.cache.SetStatus(chatID, show, status) + cacheShow := show + if presenceType == "unavailable" { + cacheShow = presenceType + } + c.cache.SetStatus(chatID, cacheShow, status) newArgs := []args.V{ gateway.SPFrom(strconv.FormatInt(chatID, 10)), @@ -1366,12 +1384,26 @@ func (c *Client) UpdateChatNicknames() { for _, id := range c.cache.ChatsKeys() { chat, ok := c.cache.GetChat(id) if ok { + newArgs := []args.V{ + gateway.SPFrom(strconv.FormatInt(id, 10)), + gateway.SPNickname(chat.Title), + } + + cachedStatus, ok := c.cache.GetStatus(id) + if ok { + show, status, typ := cachedStatus.Destruct() + newArgs = append(newArgs, gateway.SPShow(show), gateway.SPStatus(status)) + if typ != "" { + newArgs = append(newArgs, gateway.SPType(typ)) + } + } + gateway.SendPresence( c.xmpp, c.jid, - gateway.SPFrom(strconv.FormatInt(id, 10)), - gateway.SPNickname(chat.Title), + newArgs..., ) + gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) } } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 6679a72..e85dfc9 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -15,6 +15,7 @@ import ( "dev.narayana.im/narayana/telegabber/xmpp/gateway" log "github.com/sirupsen/logrus" + "github.com/soheilhy/args" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) @@ -349,11 +350,18 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { log.Error(errors.Wrap(err, "TDlib connection failure")) } else { for status := range session.StatusesRange() { + show, description, typ := status.Destruct() + newArgs := []args.V{ + gateway.SPImmed(false), + } + if typ != "" { + newArgs = append(newArgs, gateway.SPType(typ)) + } go session.ProcessStatusUpdate( status.ID, - status.Description, - status.XMPP, - gateway.SPImmed(false), + description, + show, + newArgs..., ) } session.UpdateChatNicknames() From 64515e2c666067953e3a9680b4f0db84f3838498 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 8 Aug 2023 00:54:24 -0400 Subject: [PATCH 033/228] Fix replies to messages with non-ASCII characters --- Makefile | 2 +- telegabber.go | 2 +- xmpp/handlers.go | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 33bedad..ef6aef4 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.7.5" +VERSION := "v1.7.6" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 37d5890..3802764 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.7.5" +var version string = "1.7.6" var commit string var sm *goxmpp.StreamManager diff --git a/xmpp/handlers.go b/xmpp/handlers.go index e85dfc9..36f9cf9 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -149,7 +149,12 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { "end": body.End, }).Warn(errors.Wrap(err, "Failed to parse fallback end!")) } - text = text[:start] + text[end:] + + fullRunes := []rune(text) + cutRunes := make([]rune, 0, len(text)-int(end-start)) + cutRunes = append(cutRunes, fullRunes[:start]...) + cutRunes = append(cutRunes, fullRunes[end:]...) + text = string(cutRunes) } } var replaceId int64 From 20994e29953dfc9c238f69d919912e0c26e36b97 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 28 Aug 2023 10:16:57 -0400 Subject: [PATCH 034/228] In-Band Registration (XEP-0077) --- telegram/commands.go | 54 ++++----- telegram/connect.go | 40 ++++++- xmpp/extensions/extensions.go | 36 ++++++ xmpp/gateway/gateway.go | 6 + xmpp/handlers.go | 204 +++++++++++++++++++++++++++++++++- 5 files changed, 304 insertions(+), 36 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 0c83945..b4920d4 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -15,7 +15,8 @@ import ( ) const notEnoughArguments string = "Not enough arguments" -const telegramNotInitialized string = "Telegram connection is not initialized yet" +const TelegramNotInitialized string = "Telegram connection is not initialized yet" +const TelegramAuthDone string = "Authorization is done already" const notOnline string = "Not online" var permissionsAdmin = client.ChatAdministratorRights{ @@ -244,40 +245,29 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } if cmd == "login" { - wasSessionLoginEmpty := c.Session.Login == "" - c.Session.Login = args[0] - - if wasSessionLoginEmpty && c.authorizer == nil { - go func() { - err := c.Connect(resource) - if err != nil { - log.Error(errors.Wrap(err, "TDlib connection failure")) - } - }() - // a quirk for authorizer to become ready. If it's still not, - // nothing bad: the command just needs to be resent again - time.Sleep(1e5) + err := c.TryLogin(resource, args[0]) + if err != nil { + return err.Error() } - } - if c.authorizer == nil { - return telegramNotInitialized - } - - if c.authorizer.isClosed { - return "Authorization is done already" - } - - switch cmd { - // sign in - case "login": c.authorizer.PhoneNumber <- args[0] - // check auth code - case "code": - c.authorizer.Code <- args[0] - // check auth password - case "password": - c.authorizer.Password <- args[0] + } else { + if c.authorizer == nil { + return TelegramNotInitialized + } + + if c.authorizer.isClosed { + return TelegramAuthDone + } + + switch cmd { + // check auth code + case "code": + c.authorizer.Code <- args[0] + // check auth password + case "password": + c.authorizer.Password <- args[0] + } } // sign out case "logout": diff --git a/telegram/connect.go b/telegram/connect.go index ef03428..ab9c19c 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -3,6 +3,7 @@ package telegram import ( "github.com/pkg/errors" "strconv" + "time" "dev.narayana.im/narayana/telegabber/xmpp/gateway" @@ -154,14 +155,49 @@ func (c *Client) Connect(resource string) error { log.Errorf("Could not retrieve chats: %v", err) } - gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribe")) - gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribed")) + gateway.SubscribeToTransport(c.xmpp, c.jid) gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login)) }() return nil } +func (c *Client) TryLogin(resource string, login string) error { + wasSessionLoginEmpty := c.Session.Login == "" + c.Session.Login = login + + if wasSessionLoginEmpty && c.authorizer == nil { + go func() { + err := c.Connect(resource) + if err != nil { + log.Error(errors.Wrap(err, "TDlib connection failure")) + } + }() + // a quirk for authorizer to become ready. If it's still not, + // nothing bad: just re-login again + time.Sleep(1e5) + } + + if c.authorizer == nil { + return errors.New(TelegramNotInitialized) + } + + if c.authorizer.isClosed { + return errors.New(TelegramAuthDone) + } + + return nil +} + +func (c *Client) SetPhoneNumber(login string) error { + if c.authorizer == nil || c.authorizer.isClosed { + return errors.New("Authorization not needed") + } + + c.authorizer.PhoneNumber <- login + return nil +} + // Disconnect drops TDlib connection and // returns the flag indicating if disconnecting is permitted func (c *Client) Disconnect(resource string, quit bool) bool { diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 192b630..8e2f743 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -193,6 +193,26 @@ type Replace struct { Id string `xml:"id,attr"` } +// QueryRegister is from XEP-0077 +type QueryRegister struct { + XMLName xml.Name `xml:"jabber:iq:register query"` + Instructions string `xml:"instructions"` + Username string `xml:"username"` + Registered *QueryRegisterRegistered `xml:"registered"` + Remove *QueryRegisterRemove `xml:"remove"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + +// QueryRegisterRegistered is a child element from XEP-0077 +type QueryRegisterRegistered struct { + XMLName xml.Name `xml:"registered"` +} + +// QueryRegisterRemove is a child element from XEP-0077 +type QueryRegisterRemove struct { + XMLName xml.Name `xml:"remove"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -248,6 +268,16 @@ func (c Replace) Namespace() string { return c.XMLName.Space } +// Namespace is a namespace! +func (c QueryRegister) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c QueryRegister) GetSet() *stanza.ResultSet { + return c.ResultSet +} + // Name is a packet name func (ClientMessage) Name() string { return "message" @@ -326,4 +356,10 @@ func init() { "urn:xmpp:message-correct:0", "replace", }, Replace{}) + + // register query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "jabber:iq:register", + "query", + }, QueryRegister{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 7a2500e..dfe2ebf 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -360,6 +360,12 @@ func ResumableSend(component *xmpp.Component, packet stanza.Packet) error { return err } +// SubscribeToTransport ensures a two-way subscription to the transport +func SubscribeToTransport(component *xmpp.Component, jid string) { + SendPresence(component, jid, SPType("subscribe")) + SendPresence(component, jid, SPType("subscribed")) +} + // SplitJID tokenizes a JID string to bare JID and resource func SplitJID(from string) (string, string, bool) { fromJid, err := stanza.NewJid(from) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 36f9cf9..fdcf647 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/base64" "encoding/xml" + "fmt" "github.com/pkg/errors" "io" "strconv" @@ -57,6 +58,22 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetDiscoInfo(s, iq) return } + _, ok = iq.Payload.(*stanza.DiscoItems) + if ok { + go handleGetDiscoItems(s, iq) + return + } + _, ok = iq.Payload.(*extensions.QueryRegister) + if ok { + go handleGetQueryRegister(s, iq) + return + } + } else if iq.Type == "set" { + query, ok := iq.Payload.(*extensions.QueryRegister) + if ok { + go handleSetQueryRegister(s, iq, query) + return + } } } @@ -91,8 +108,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { session, ok := sessions[bare] if !ok { if msg.To == gatewayJid { - gateway.SendPresence(component, msg.From, gateway.SPType("subscribe")) - gateway.SendPresence(component, msg.From, gateway.SPType("subscribed")) + gateway.SubscribeToTransport(component, msg.From) } else { log.Error("Message from stranger") } @@ -444,6 +460,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { disco.AddIdentity("", "account", "registered") } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + disco.AddFeatures("jabber:iq:register") } answer.Payload = disco @@ -458,6 +475,189 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { _ = gateway.ResumableSend(component, answer) } +func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) { + 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 + } + + answer.Payload = answer.DiscoItems() + + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + _ = gateway.ResumableSend(component, answer) +} + +func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) { + 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 + } + + var login string + bare, _, ok := gateway.SplitJID(iq.From) + if ok { + session, ok := sessions[bare] + if ok { + login = session.Session.Login + } + } + + var query stanza.IQPayload + if login == "" { + query = extensions.QueryRegister{ + Instructions: fmt.Sprintf("Authorization in Telegram is a multi-step process, so please accept %v to your contacts and follow further instructions (provide the authentication code there, etc.).\nFor now, please provide your login.", iq.To), + } + } else { + query = extensions.QueryRegister{ + Instructions: "Already logged in", + Username: login, + Registered: &extensions.QueryRegisterRegistered{}, + } + } + answer.Payload = query + + log.Debugf("%#v", query) + + _ = gateway.ResumableSend(component, answer) + + if login == "" { + gateway.SubscribeToTransport(component, iq.From) + } +} + +func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { + 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) + + if query.Remove != nil { + iqAnswerSetError(answer, query, 405) + return + } + + var login string + var session *telegram.Client + bare, resource, ok := gateway.SplitJID(iq.From) + if ok { + session, ok = sessions[bare] + if ok { + login = session.Session.Login + } + } + + if login == "" { + if !ok { + session, ok = getTelegramInstance(bare, &persistence.Session{}, component) + if !ok { + iqAnswerSetError(answer, query, 500) + return + } + } + + err := session.TryLogin(resource, query.Username) + if err != nil { + if err.Error() == telegram.TelegramAuthDone { + iqAnswerSetError(answer, query, 406) + } else { + iqAnswerSetError(answer, query, 500) + } + return + } + + err = session.SetPhoneNumber(query.Username) + if err != nil { + iqAnswerSetError(answer, query, 500) + return + } + + // everything okay, the response should be empty with no payload/error at this point + gateway.SubscribeToTransport(component, iq.From) + } else { + iqAnswerSetError(answer, query, 406) + } +} + +func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { + answer.Type = stanza.IQTypeError + answer.Payload = *payload + switch code { + case 400: + answer.Error = &stanza.Err{ + Code: code, + Type: stanza.ErrorTypeModify, + Reason: "bad-request", + } + case 405: + answer.Error = &stanza.Err{ + Code: code, + Type: stanza.ErrorTypeCancel, + Reason: "not-allowed", + Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport", + } + case 406: + answer.Error = &stanza.Err{ + Code: code, + Type: stanza.ErrorTypeModify, + Reason: "not-acceptable", + Text: "Phone number already provided, chat with the transport for further instruction", + } + case 500: + answer.Error = &stanza.Err{ + Code: code, + Type: stanza.ErrorTypeWait, + Reason: "internal-server-error", + } + default: + log.Error("Unknown error code, falling back with empty reason") + answer.Error = &stanza.Err{ + Code: code, + Type: stanza.ErrorTypeCancel, + Reason: "undefined-condition", + } + } +} + func toToID(to string) (int64, bool) { toParts := strings.Split(to, "@") if len(toParts) < 2 { From aa561c5be606c14cfd211df694ef0be856195df7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 28 Aug 2023 10:20:50 -0400 Subject: [PATCH 035/228] Version 1.8.0 --- Makefile | 2 +- telegabber.go | 2 +- telegram/utils.go | 8 ++++---- xmpp/handlers.go | 24 ++++++++++++------------ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index d6323d0..724c73d 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.8.0-dev" +VERSION := "v1.8.0" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 671e13b..f409599 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.8.0-dev" +var version string = "1.8.0" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index 4d77fc4..47a851a 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -249,15 +249,15 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o presenceType = typ } log.WithFields(log.Fields{ - "show": show, - "status": status, + "show": show, + "status": status, "presenceType": presenceType, }).Debug("Cached status") } else if user != nil && user.Status != nil { show, status, presenceType = c.userStatusToText(user.Status, chatID) log.WithFields(log.Fields{ - "show": show, - "status": status, + "show": show, + "status": status, "presenceType": presenceType, }).Debug("Status to text") } else { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index fdcf647..4c27b3c 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -624,35 +624,35 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code switch code { case 400: answer.Error = &stanza.Err{ - Code: code, - Type: stanza.ErrorTypeModify, + Code: code, + Type: stanza.ErrorTypeModify, Reason: "bad-request", } case 405: answer.Error = &stanza.Err{ - Code: code, - Type: stanza.ErrorTypeCancel, + Code: code, + Type: stanza.ErrorTypeCancel, Reason: "not-allowed", - Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport", + Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport", } case 406: answer.Error = &stanza.Err{ - Code: code, - Type: stanza.ErrorTypeModify, + Code: code, + Type: stanza.ErrorTypeModify, Reason: "not-acceptable", - Text: "Phone number already provided, chat with the transport for further instruction", + Text: "Phone number already provided, chat with the transport for further instruction", } case 500: answer.Error = &stanza.Err{ - Code: code, - Type: stanza.ErrorTypeWait, + Code: code, + Type: stanza.ErrorTypeWait, Reason: "internal-server-error", } default: log.Error("Unknown error code, falling back with empty reason") answer.Error = &stanza.Err{ - Code: code, - Type: stanza.ErrorTypeCancel, + Code: code, + Type: stanza.ErrorTypeCancel, Reason: "undefined-condition", } } From 4588170d1e43db780c551177f5996598fe25bc6e Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 31 Aug 2023 17:26:35 -0400 Subject: [PATCH 036/228] Harden the authorizer access to prevent crashes --- Makefile | 2 +- telegabber.go | 2 +- telegram/client.go | 3 +++ telegram/commands.go | 6 ++++++ telegram/connect.go | 24 ++++++++++++++++++++++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 724c73d..e139c00 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.8.0" +VERSION := "v1.8.1" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index f409599..85c5fbd 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.8.0" +var version string = "1.8.1" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/client.go b/telegram/client.go index e9acd20..6f6d719 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -74,6 +74,9 @@ type clientLocks struct { resourcesLock sync.Mutex outboxLock sync.Mutex lastMsgHashesLock sync.Mutex + + authorizerReadLock sync.Mutex + authorizerWriteLock sync.Mutex } // NewClient instantiates a Telegram App diff --git a/telegram/commands.go b/telegram/commands.go index b4920d4..b729973 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -244,6 +244,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string return notEnoughArguments } + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() + if cmd == "login" { err := c.TryLogin(resource, args[0]) if err != nil { @@ -324,10 +327,13 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string lastname = rawCmdArguments(cmdline, 1) } + c.locks.authorizerWriteLock.Lock() if c.authorizer != nil && !c.authorizer.isClosed { c.authorizer.FirstName <- firstname c.authorizer.LastName <- lastname + c.locks.authorizerWriteLock.Unlock() } else { + c.locks.authorizerWriteLock.Unlock() if !c.Online() { return notOnline } diff --git a/telegram/connect.go b/telegram/connect.go index ab9c19c..6c49aa9 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -110,6 +110,7 @@ func (c *Client) Connect(resource string) error { log.Warn("Connecting to Telegram network...") + c.locks.authorizerWriteLock.Lock() c.authorizer = &clientAuthorizer{ TdlibParameters: make(chan *client.SetTdlibParametersRequest, 1), PhoneNumber: make(chan string, 1), @@ -123,6 +124,7 @@ func (c *Client) Connect(resource string) error { go c.interactor() c.authorizer.TdlibParameters <- c.parameters + c.locks.authorizerWriteLock.Unlock() tdlibClient, err := client.NewClient(c.authorizer, c.options...) if err != nil { @@ -178,6 +180,9 @@ func (c *Client) TryLogin(resource string, login string) error { time.Sleep(1e5) } + c.locks.authorizerReadLock.Lock() + defer c.locks.authorizerReadLock.Unlock() + if c.authorizer == nil { return errors.New(TelegramNotInitialized) } @@ -190,6 +195,9 @@ func (c *Client) TryLogin(resource string, login string) error { } func (c *Client) SetPhoneNumber(login string) error { + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() + if c.authorizer == nil || c.authorizer.isClosed { return errors.New("Authorization not needed") } @@ -234,9 +242,16 @@ func (c *Client) Disconnect(resource string, quit bool) bool { func (c *Client) interactor() { for { + c.locks.authorizerReadLock.Lock() + if c.authorizer == nil { + log.Warn("Authorizer is lost, halting the interactor") + c.locks.authorizerReadLock.Unlock() + return + } state, ok := <-c.authorizer.State if !ok { log.Warn("Interactor is disconnected") + c.locks.authorizerReadLock.Unlock() return } @@ -266,18 +281,27 @@ func (c *Client) interactor() { log.Warn("Waiting for 2FA password...") gateway.SendServiceMessage(c.jid, "Please, enter 2FA passphrase via /password 12345", c.xmpp) } + c.locks.authorizerReadLock.Unlock() } } func (c *Client) forceClose() { + c.locks.authorizerReadLock.Lock() + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerReadLock.Unlock() + defer c.locks.authorizerWriteLock.Unlock() + c.online = false c.authorizer = nil } func (c *Client) close() { + c.locks.authorizerWriteLock.Lock() if c.authorizer != nil && !c.authorizer.isClosed { c.authorizer.Close() } + c.locks.authorizerWriteLock.Unlock() + if c.client != nil { _, err := c.client.Close() if err != nil { From 282a6fc21b9626ab1bdc9c5a78162d90b7d28aa2 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 31 Aug 2023 18:24:30 -0400 Subject: [PATCH 037/228] Hotfix: prevent lockup on login --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 9 ++++++--- telegram/connect.go | 5 +++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index e139c00..5c001af 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "8517026415e75a8eec567774072cbbbbb52376c1" -VERSION := "v1.8.1" +VERSION := "v1.8.2" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 85c5fbd..a1efd12 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.8.1" +var version string = "1.8.2" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index b729973..87fff72 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -244,17 +244,20 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string return notEnoughArguments } - c.locks.authorizerWriteLock.Lock() - defer c.locks.authorizerWriteLock.Unlock() - if cmd == "login" { err := c.TryLogin(resource, args[0]) if err != nil { return err.Error() } + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() + c.authorizer.PhoneNumber <- args[0] } else { + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() + if c.authorizer == nil { return TelegramNotInitialized } diff --git a/telegram/connect.go b/telegram/connect.go index 6c49aa9..b1b8b10 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -122,6 +122,7 @@ func (c *Client) Connect(resource string) error { } go c.interactor() + log.Warn("Interactor launched") c.authorizer.TdlibParameters <- c.parameters c.locks.authorizerWriteLock.Unlock() @@ -180,8 +181,8 @@ func (c *Client) TryLogin(resource string, login string) error { time.Sleep(1e5) } - c.locks.authorizerReadLock.Lock() - defer c.locks.authorizerReadLock.Unlock() + c.locks.authorizerWriteLock.Lock() + defer c.locks.authorizerWriteLock.Unlock() if c.authorizer == nil { return errors.New(TelegramNotInitialized) From 776993894ad780f1500c139aff85378c8a1d22f5 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 17 Sep 2023 00:54:23 -0400 Subject: [PATCH 038/228] Merge hotfix: remove redundant "registered" identity --- xmpp/handlers.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 96a30c4..6f51427 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -463,9 +463,7 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { if dt == discoTypeInfo { disco := answer.DiscoInfo() toID, toOk := toToID(iq.To) - if toOk { - disco.AddIdentity("", "account", "registered") - } else { + if !toOk { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddFeatures("jabber:iq:register") } From f99f4f6acc6734ecbd0da80015285e6b3d39fdd8 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 17 Sep 2023 23:21:57 -0400 Subject: [PATCH 039/228] Send memberlist on MUC join, suppress PM statuses for MUC JIDs --- telegram/utils.go | 55 ++++++++++++++++++++++ xmpp/extensions/extensions.go | 24 ++++++++++ xmpp/gateway/gateway.go | 24 ++++++++++ xmpp/handlers.go | 86 +++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index 08c45d2..3ab6b88 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -217,6 +217,10 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o return err } + if chat != nil && c.Session.MUC && c.IsGroup(chat) { + return nil + } + var photo string if chat != nil && chat.Photo != nil { file, path, err := c.ForceOpenFile(chat.Photo.Small, 1) @@ -290,6 +294,35 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o ) } +func (c *Client) SendMUCStatuses(chatID int64) { + members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ + ChatId: chatID, + Limit: 200, + Filter: &client.ChatMembersFilterMembers{}, + }) + if err == nil { + 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 + } + gateway.SendPresence( + c.xmpp, + c.jid, + gateway.SPFrom(strconv.FormatInt(chatID, 10)), + gateway.SPResource(c.formatContact(senderId)), + gateway.SPImmed(true), + gateway.SPAffiliation(c.memberStatusToAffiliation(member.Status)), + ) + } + } +} + func (c *Client) formatContact(chatID int64) string { if chatID == 0 { return "" @@ -1434,6 +1467,10 @@ func (c *Client) UpdateChatNicknames() { for _, id := range c.cache.ChatsKeys() { chat, ok := c.cache.GetChat(id) if ok { + if c.Session.MUC && c.IsGroup(chat) { + continue + } + newArgs := []args.V{ gateway.SPFrom(strconv.FormatInt(id, 10)), gateway.SPNickname(chat.Title), @@ -1560,3 +1597,21 @@ func (c *Client) usernamesToString(usernames []string) string { } return strings.Join(atUsernames, ", ") } + +func (c *Client) memberStatusToAffiliation(memberStatus client.ChatMemberStatus) string { + switch memberStatus.ChatMemberStatusType() { + case client.TypeChatMemberStatusCreator: + return "owner" + case client.TypeChatMemberStatusAdministrator: + return "admin" + case client.TypeChatMemberStatusMember: + return "member" + case client.TypeChatMemberStatusRestricted: + return "outcast" + case client.TypeChatMemberStatusLeft: + return "none" + case client.TypeChatMemberStatusBanned: + return "outcast" + } + return "member" +} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 8e2f743..0b7269f 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -213,6 +213,19 @@ type QueryRegisterRemove struct { XMLName xml.Name `xml:"remove"` } +// PresenceXMucUserExtension is from XEP-0045 +type PresenceXMucUserExtension struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` + Item PresenceXMucUserItem +} + +// PresenceXMucUserItem is from XEP-0045 +type PresenceXMucUserItem struct { + XMLName xml.Name `xml:"item"` + Affiliation string `xml:"affiliation,attr"` + Role string `xml:"role,attr"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -278,6 +291,11 @@ func (c QueryRegister) GetSet() *stanza.ResultSet { return c.ResultSet } +// Namespace is a namespace! +func (c PresenceXMucUserExtension) Namespace() string { + return c.XMLName.Space +} + // Name is a packet name func (ClientMessage) Name() string { return "message" @@ -362,4 +380,10 @@ func init() { "jabber:iq:register", "query", }, QueryRegister{}) + + // presence muc user + stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{ + "http://jabber.org/protocol/muc#user", + "x", + }, PresenceXMucUserExtension{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index dfe2ebf..c09a061 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -240,6 +240,9 @@ var SPResource = args.NewString() // SPImmed skips queueing var SPImmed = args.NewBool(args.Default(true)) +// SPAffiliation is a XEP-0045 MUC affiliation +var SPAffiliation = args.NewString() + func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { var presenceFrom string if SPFrom.IsSet(args) { @@ -295,6 +298,17 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { }) } } + if SPAffiliation.IsSet(args) { + affiliation := SPAffiliation.Get(args) + if affiliation != "" { + presence.Extensions = append(presence.Extensions, extensions.PresenceXMucUserExtension{ + Item: extensions.PresenceXMucUserItem{ + Affiliation: affiliation, + Role: affilationToRole(affiliation), + }, + }) + } + } return presence } @@ -377,3 +391,13 @@ func SplitJID(from string) (string, string, bool) { } return fromJid.Bare(), fromJid.Resource, true } + +func affilationToRole(affilation string) string { + switch affilation { + case "owner", "admin": + return "moderator" + case "member": + return "participant" + } + return "none" +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 6f51427..a1f6d74 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -287,6 +287,12 @@ func HandlePresence(s xmpp.Sender, p stanza.Packet) { } if prs.To == gateway.Jid.Bare() { handlePresence(s, prs) + return + } + var mucExt stanza.MucPresence + prs.Get(&mucExt) + if mucExt.XMLName.Space != "" { + handleMUCPresence(s, prs) } } @@ -397,6 +403,64 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { } } +func handleMUCPresence(s xmpp.Sender, p stanza.Presence) { + log.WithFields(log.Fields{ + "type": p.Type, + "from": p.From, + "to": p.To, + }).Warn("MUC presence") + log.Debugf("%#v", p) + + if p.Type == "" { + toBare, nickname, ok := gateway.SplitJID(p.To) + if ok { + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + reply := stanza.Presence{Attrs: stanza.Attrs{ + From: toBare, + To: p.From, + Id: p.Id, + }} + defer gateway.ResumableSend(component, reply) + + if nickname == "" { + presenceReplySetError(&reply, 400) + return + } + + chatId, ok := toToID(toBare) + if !ok { + presenceReplySetError(&reply, 404) + return + } + + fromBare, _, ok := gateway.SplitJID(p.From) + if !ok { + presenceReplySetError(&reply, 400) + return + } + + session, ok := sessions[fromBare] + if !ok || !session.Session.MUC { + presenceReplySetError(&reply, 401) + return + } + + chat, _, err := session.GetContactByID(chatId, nil) + if err != nil || !session.IsGroup(chat) { + presenceReplySetError(&reply, 404) + return + } + + session.SendMUCStatuses(chatId) + } + } +} + func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { log.WithFields(log.Fields{ "from": iq.From, @@ -711,6 +775,28 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code } } +func presenceReplySetError(reply *stanza.Presence, code int) { + reply.Type = stanza.PresenceTypeError + reply.Error = stanza.Err{ + Code: code, + } + switch code { + case 400: + reply.Error.Type = stanza.ErrorTypeModify + reply.Error.Reason = "jid-malformed" + case 401: + reply.Error.Type = stanza.ErrorTypeAuth + reply.Error.Reason = "not-authorized" + case 404: + reply.Error.Type = stanza.ErrorTypeCancel + reply.Error.Reason = "item-not-found" + default: + log.Error("Unknown error code, falling back with empty reason") + reply.Error.Type = stanza.ErrorTypeCancel + reply.Error.Reason = "undefined-condition" + } +} + func toToID(to string) (int64, bool) { toParts := strings.Split(to, "@") if len(toParts) < 2 { From 4249a8bf41513f14bdeead32c6cbc00389f9db74 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 18 Sep 2023 00:02:49 -0400 Subject: [PATCH 040/228] Suppress nickname presences for MUCs better --- telegram/handlers.go | 6 +++++- telegram/utils.go | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 0d1cda9..71b55bc 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -315,10 +315,14 @@ func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) // chat title changed func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { + chat, user, _ := c.GetContactByID(update.ChatId, nil) + if c.Session.MUC && c.IsGroup(chat) { + return + } + gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) // set also the status (for group chats only) - chat, user, _ := c.GetContactByID(update.ChatId, nil) if user == nil { c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true)) } diff --git a/telegram/utils.go b/telegram/utils.go index 3ab6b88..c12321d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1410,6 +1410,10 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) { chat, _, _ = c.GetContactByID(id, nil) } if chat != nil { + if c.Session.MUC && c.IsGroup(chat) { + return + } + args = append(args, gateway.SPNickname(chat.Title)) gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) From 6c65ef9988dc786a6b634e05f4e40aacdf9191cb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 18 Sep 2023 00:47:47 -0400 Subject: [PATCH 041/228] Send the own MUC member the last with status codes 110/210 according to the spec --- telegram/utils.go | 37 ++++++++++++++++++++++++++++++++--- xmpp/extensions/extensions.go | 11 +++++++++-- xmpp/gateway/gateway.go | 16 +++++++++++++-- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index c12321d..a281a90 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -301,6 +301,17 @@ func (c *Client) SendMUCStatuses(chatID int64) { Filter: &client.ChatMembersFilterMembers{}, }) if err == nil { + chatIDString := strconv.FormatInt(chatID, 10) + + myNickname := "me" + if c.me != nil { + myNickname := c.me.FirstName + if c.me.LastName != "" { + myNickname = myNickname + " " + c.me.LastName + } + } + myAffiliation := "member" + for _, member := range members.Members { var senderId int64 switch member.MemberId.MessageSenderType() { @@ -311,15 +322,35 @@ func (c *Client) SendMUCStatuses(chatID int64) { memberChat, _ := member.MemberId.(*client.MessageSenderChat) senderId = memberChat.ChatId } + + nickname := c.formatContact(senderId) + affiliation := c.memberStatusToAffiliation(member.Status) + if c.me != nil && senderId == c.me.Id { + myNickname = nickname + myAffiliation = affiliation + continue + } + gateway.SendPresence( c.xmpp, c.jid, - gateway.SPFrom(strconv.FormatInt(chatID, 10)), - gateway.SPResource(c.formatContact(senderId)), + gateway.SPFrom(chatIDString), + gateway.SPResource(nickname), gateway.SPImmed(true), - gateway.SPAffiliation(c.memberStatusToAffiliation(member.Status)), + gateway.SPAffiliation(affiliation), ) } + + // according to the spec, own member entry should be sent the last + gateway.SendPresence( + c.xmpp, + c.jid, + gateway.SPFrom(chatIDString), + gateway.SPResource(myNickname), + gateway.SPImmed(true), + gateway.SPAffiliation(myAffiliation), + gateway.SPMUCStatusCodes([]uint16{110, 210}), + ) } } diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 0b7269f..1c32fcd 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -215,8 +215,9 @@ type QueryRegisterRemove struct { // PresenceXMucUserExtension is from XEP-0045 type PresenceXMucUserExtension struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` - Item PresenceXMucUserItem + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` + Item PresenceXMucUserItem + Statuses []PresenceXMucUserStatus } // PresenceXMucUserItem is from XEP-0045 @@ -226,6 +227,12 @@ type PresenceXMucUserItem struct { Role string `xml:"role,attr"` } +// PresenceXMucUserStatus is from XEP-0045 +type PresenceXMucUserStatus struct { + XMLName xml.Name `xml:"status"` + Code uint16 `xml:"code,attr"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index c09a061..e4a1be7 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -243,6 +243,9 @@ var SPImmed = args.NewBool(args.Default(true)) // SPAffiliation is a XEP-0045 MUC affiliation var SPAffiliation = args.NewString() +// SPMUCStatusCodes is a set of XEP-0045 MUC status codes +var SPMUCStatusCodes = args.New() + func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { var presenceFrom string if SPFrom.IsSet(args) { @@ -301,12 +304,21 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { if SPAffiliation.IsSet(args) { affiliation := SPAffiliation.Get(args) if affiliation != "" { - presence.Extensions = append(presence.Extensions, extensions.PresenceXMucUserExtension{ + userExt := extensions.PresenceXMucUserExtension{ Item: extensions.PresenceXMucUserItem{ Affiliation: affiliation, Role: affilationToRole(affiliation), }, - }) + } + if SPMUCStatusCodes.IsSet(args) { + statusCodes := SPMUCStatusCodes.Get(args).([]uint16) + for _, statusCode := range statusCodes { + userExt.Statuses = append(userExt.Statuses, extensions.PresenceXMucUserStatus{ + Code: statusCode, + }) + } + } + presence.Extensions = append(presence.Extensions, userExt) } } From 93abbe834ee10bf243af9538202bd435e9be1cb6 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 18 Sep 2023 01:17:25 -0400 Subject: [PATCH 042/228] Send real JID for room occupants --- telegram/utils.go | 8 +++++--- xmpp/extensions/extensions.go | 1 + xmpp/gateway/gateway.go | 14 ++++++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index a281a90..9ecf7af 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -302,6 +302,7 @@ func (c *Client) SendMUCStatuses(chatID int64) { }) if err == nil { chatIDString := strconv.FormatInt(chatID, 10) + gatewayJidSuffix := "@" + gateway.Jid.Full() myNickname := "me" if c.me != nil { @@ -337,7 +338,8 @@ func (c *Client) SendMUCStatuses(chatID int64) { gateway.SPFrom(chatIDString), gateway.SPResource(nickname), gateway.SPImmed(true), - gateway.SPAffiliation(affiliation), + gateway.SPMUCAffiliation(affiliation), + gateway.SPMUCJid(strconv.FormatInt(senderId, 10) + gatewayJidSuffix), ) } @@ -348,8 +350,8 @@ func (c *Client) SendMUCStatuses(chatID int64) { gateway.SPFrom(chatIDString), gateway.SPResource(myNickname), gateway.SPImmed(true), - gateway.SPAffiliation(myAffiliation), - gateway.SPMUCStatusCodes([]uint16{110, 210}), + gateway.SPMUCAffiliation(myAffiliation), + gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), ) } } diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 1c32fcd..3a2f998 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -224,6 +224,7 @@ type PresenceXMucUserExtension struct { type PresenceXMucUserItem struct { XMLName xml.Name `xml:"item"` Affiliation string `xml:"affiliation,attr"` + Jid string `xml:"jid,attr"` Role string `xml:"role,attr"` } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index e4a1be7..d6b7826 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -240,8 +240,11 @@ var SPResource = args.NewString() // SPImmed skips queueing var SPImmed = args.NewBool(args.Default(true)) -// SPAffiliation is a XEP-0045 MUC affiliation -var SPAffiliation = args.NewString() +// SPMUCAffiliation is a XEP-0045 MUC affiliation +var SPMUCAffiliation = args.NewString() + +// SPMUCJid is a real jid of a MUC member +var SPMUCJid = args.NewString() // SPMUCStatusCodes is a set of XEP-0045 MUC status codes var SPMUCStatusCodes = args.New() @@ -301,8 +304,8 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { }) } } - if SPAffiliation.IsSet(args) { - affiliation := SPAffiliation.Get(args) + if SPMUCAffiliation.IsSet(args) { + affiliation := SPMUCAffiliation.Get(args) if affiliation != "" { userExt := extensions.PresenceXMucUserExtension{ Item: extensions.PresenceXMucUserItem{ @@ -310,6 +313,9 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { Role: affilationToRole(affiliation), }, } + if SPMUCJid.IsSet(args) { + userExt.Item.Jid = SPMUCJid.Get(args) + } if SPMUCStatusCodes.IsSet(args) { statusCodes := SPMUCStatusCodes.Get(args).([]uint16) for _, statusCode := range statusCodes { From c1887e5a1ed80fd06795a1017c821375562ff70b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 18 Sep 2023 01:49:31 -0400 Subject: [PATCH 043/228] Fix returning MUC join errors --- xmpp/handlers.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a1f6d74..a7e3c55 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -420,7 +420,9 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence) { return } - reply := stanza.Presence{Attrs: stanza.Attrs{ + // separate declaration is crucial for passing as pointer to defer + var reply *stanza.Presence + reply = &stanza.Presence{Attrs: stanza.Attrs{ From: toBare, To: p.From, Id: p.Id, @@ -428,31 +430,31 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence) { defer gateway.ResumableSend(component, reply) if nickname == "" { - presenceReplySetError(&reply, 400) + presenceReplySetError(reply, 400) return } chatId, ok := toToID(toBare) if !ok { - presenceReplySetError(&reply, 404) + presenceReplySetError(reply, 404) return } fromBare, _, ok := gateway.SplitJID(p.From) if !ok { - presenceReplySetError(&reply, 400) + presenceReplySetError(reply, 400) return } session, ok := sessions[fromBare] if !ok || !session.Session.MUC { - presenceReplySetError(&reply, 401) + presenceReplySetError(reply, 401) return } chat, _, err := session.GetContactByID(chatId, nil) if err != nil || !session.IsGroup(chat) { - presenceReplySetError(&reply, 404) + presenceReplySetError(reply, 404) return } From e77caf2c42c079062ade31f928a40b7654ac9bfd Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 19 Sep 2023 04:23:39 -0400 Subject: [PATCH 044/228] Send recent history on MUC join --- telegram/client.go | 4 + telegram/commands.go | 48 +------- telegram/handlers.go | 2 +- telegram/utils.go | 206 +++++++++++++++++++++++++++------- xmpp/extensions/extensions.go | 36 ++++++ xmpp/gateway/gateway.go | 55 ++++++--- xmpp/handlers.go | 4 +- 7 files changed, 256 insertions(+), 99 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 6f6d719..6033c7c 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -64,6 +64,8 @@ type Client struct { lastMsgHashes map[int64]uint64 msgHashSeed maphash.Seed + mucResources map[int64]map[string]bool + locks clientLocks SendMessageLock sync.Mutex } @@ -73,6 +75,7 @@ type clientLocks struct { chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex outboxLock sync.Mutex + mucResourcesLock sync.Mutex lastMsgHashesLock sync.Mutex authorizerReadLock sync.Mutex @@ -133,6 +136,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component Session: session, resources: make(map[string]bool), outbox: make(map[string]string), + mucResources: make(map[int64]map[string]bool), content: &conf.Content, cache: cache.NewCache(), options: options, diff --git a/telegram/commands.go b/telegram/commands.go index 87fff72..d365d4e 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -193,23 +193,6 @@ 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) - - gateway.SendMessage( - c.jid, - strconv.FormatInt(chatID, 10), - c.formatMessage(0, 0, false, message), - strconv.FormatInt(message.Id, 10), - c.xmpp, - reply, - false, - ) - } -} - func (c *Client) usernameOrIDToID(username string) (int64, error) { userID, err := strconv.ParseInt(username, 10, 64) // couldn't parse the id, try to lookup as a username @@ -1005,7 +988,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return err.Error(), true } - c.sendMessagesReverse(chatID, messages.Messages) + c.sendMessagesReverse(chatID, messages.Messages, true, "") // get latest entries from history case "history": var limit int32 = 10 @@ -1016,32 +999,11 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } } - var newMessages *client.Messages - var messages []*client.Message - var err error - var fromId int64 - for _ = range make([]struct{}, limit) { // safety limit - if len(messages) > 0 { - fromId = messages[len(messages)-1].Id - } - - newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{ - ChatId: chatID, - FromMessageId: fromId, - Limit: limit, - }) - if err != nil { - return err.Error(), true - } - - messages = append(messages, newMessages.Messages...) - - if len(newMessages.Messages) == 0 || len(messages) >= int(limit) { - break - } + messages, err := c.getNLastMessages(chatID, limit) + if err != nil { + return err.Error(), true } - - c.sendMessagesReverse(chatID, messages) + c.sendMessagesReverse(chatID, messages, true, "") // chat members case "members": var query string diff --git a/telegram/handlers.go b/telegram/handlers.go index 71b55bc..b7277e8 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -265,7 +265,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, 0, false, false) } } } diff --git a/telegram/utils.go b/telegram/utils.go index 9ecf7af..036956b 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -44,6 +44,12 @@ var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" const messageHeaderSeparator string = " | " +const ( + ChatTypeOther byte = iota + ChatTypePM + ChatTypeGroup +) + // 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() { @@ -121,10 +127,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 checks if a chat is PM or group +func (c *Client) GetChatType(id int64) (byte, error) { if !c.Online() || id == 0 { - return false, errOffline + return ChatTypeOther, errOffline } var err error @@ -135,7 +141,7 @@ func (c *Client) IsPM(id int64) (bool, error) { ChatId: id, }) if err != nil { - return false, err + return ChatTypeOther, err } c.cache.SetChat(id, chat) @@ -143,9 +149,12 @@ func (c *Client) IsPM(id int64) (bool, error) { chatType := chat.Type.ChatTypeType() if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret { - return true, nil + return ChatTypePM, nil } - return false, nil + if c.IsGroup(chat) { + return ChatTypeGroup, nil + } + return ChatTypeOther, nil } func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { @@ -294,25 +303,52 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o ) } -func (c *Client) SendMUCStatuses(chatID int64) { +// JoinMUC saves MUC join fact and sends initialization data +func (c *Client) JoinMUC(chatId int64, resource string) { + // save the nickname in this MUC, also as a marker of join + c.locks.mucResourcesLock.Lock() + oldMap, ok := c.mucResources[chatId] + if ok { + _, ok := oldMap[resource] + if ok { + // already joined, initializing anyway + } else { + oldMap[resource] = true + } + } else { + newMap := make(map[string]bool) + newMap[resource] = true + c.mucResources[chatId] = newMap + } + c.locks.mucResourcesLock.Unlock() + + c.sendMUCStatuses(chatId) + + messages, err := c.getNLastMessages(chatId, 20) + if err == nil { + c.sendMessagesReverse(chatId, messages, false, c.jid+"/"+resource) + } +} + +func (c *Client) sendMUCStatuses(chatID int64) { + sChatId := strconv.FormatInt(chatID, 10) + myNickname := "me" + if c.me != nil { + myNickname := c.me.FirstName + if c.me.LastName != "" { + myNickname = myNickname + " " + c.me.LastName + } + } + myAffiliation := "member" + members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ ChatId: chatID, Limit: 200, Filter: &client.ChatMembersFilterMembers{}, }) if err == nil { - chatIDString := strconv.FormatInt(chatID, 10) gatewayJidSuffix := "@" + gateway.Jid.Full() - myNickname := "me" - if c.me != nil { - myNickname := c.me.FirstName - if c.me.LastName != "" { - myNickname = myNickname + " " + c.me.LastName - } - } - myAffiliation := "member" - for _, member := range members.Members { var senderId int64 switch member.MemberId.MessageSenderType() { @@ -324,7 +360,7 @@ func (c *Client) SendMUCStatuses(chatID int64) { senderId = memberChat.ChatId } - nickname := c.formatContact(senderId) + nickname := c.getMUCNickname(senderId) affiliation := c.memberStatusToAffiliation(member.Status) if c.me != nil && senderId == c.me.Id { myNickname = nickname @@ -335,25 +371,29 @@ func (c *Client) SendMUCStatuses(chatID int64) { gateway.SendPresence( c.xmpp, c.jid, - gateway.SPFrom(chatIDString), + gateway.SPFrom(sChatId), gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(affiliation), gateway.SPMUCJid(strconv.FormatInt(senderId, 10) + gatewayJidSuffix), ) } - - // according to the spec, own member entry should be sent the last - gateway.SendPresence( - c.xmpp, - c.jid, - gateway.SPFrom(chatIDString), - gateway.SPResource(myNickname), - gateway.SPImmed(true), - gateway.SPMUCAffiliation(myAffiliation), - gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), - ) } + + // according to the spec, own member entry should be sent the last + gateway.SendPresence( + c.xmpp, + c.jid, + gateway.SPFrom(sChatId), + gateway.SPResource(myNickname), + gateway.SPImmed(true), + gateway.SPMUCAffiliation(myAffiliation), + gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), + ) +} + +func (c *Client) getMUCNickname(chatID int64) string { + return c.formatContact(chatID) } func (c *Client) formatContact(chatID int64) string { @@ -917,13 +957,13 @@ func (c *Client) countCharsInLines(lines *[]string) (count int) { } func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) { - isPM, err := c.IsPM(message.ChatId) + chatType, err := c.GetChatType(message.ChatId) if err != nil { - log.Errorf("Could not determine if chat is PM: %v", err) + log.Errorf("Could not determine chat type: %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 := (isCarbonsEnabled && (message.IsOutgoing || chatType == ChatTypePM)) || (c.Session.MUC && chatType == ChatTypeGroup) var replyStart, replyEnd int prefix := []string{} @@ -944,7 +984,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } } } - if !isPM || !c.Session.HideIds { + if chatType != ChatTypePM || !c.Session.HideIds { prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) } // show sender in group chats @@ -999,8 +1039,20 @@ 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 - jids := c.getCarbonFullJids(isCarbon, "") + c.sendMessageToGateway(chatId, message, false, "", []string{}) +} + +func (c *Client) sendMessageToGateway(chatId int64, message *client.Message, delay bool, groupChatFrom string, groupChatTos []string) { + var isCarbon bool + var jids []string + var isGroupchat bool + if len(groupChatTos) == 0 { + isCarbon = gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing + jids = c.getCarbonFullJids(isCarbon, "") + } else { + isGroupchat = true + jids = groupChatTos + } var text, oob, auxText string @@ -1073,12 +1125,22 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { // forward message to XMPP sId := strconv.FormatInt(message.Id, 10) - sChatId := strconv.FormatInt(chatId, 10) + var from string + if groupChatFrom == "" { + from = strconv.FormatInt(chatId, 10) + } else { + from = groupChatFrom + } + + var timestamp int64 + if delay { + timestamp = int64(message.Date) + } for _, jid := range jids { - gateway.SendMessageWithOOB(jid, sChatId, text, sId, c.xmpp, reply, oob, isCarbon) + gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, isCarbon, isGroupchat) if auxText != "" { - gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isCarbon) + gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, isCarbon, isGroupchat) } } } @@ -1294,6 +1356,36 @@ func (c *Client) getLastMessages(id int64, query string, from int64, count int32 }) } +func (c *Client) getNLastMessages(chatID int64, limit int32) ([]*client.Message, error) { + var newMessages *client.Messages + var messages []*client.Message + var err error + var fromId int64 + + for _ = range make([]struct{}, limit) { // safety limit + if len(messages) > 0 { + fromId = messages[len(messages)-1].Id + } + + newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{ + ChatId: chatID, + FromMessageId: fromId, + Limit: limit, + }) + if err != nil { + return nil, err + } + + messages = append(messages, newMessages.Messages...) + + if len(newMessages.Messages) == 0 || len(messages) >= int(limit) { + break + } + } + + return messages, nil +} + // 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{ @@ -1652,3 +1744,39 @@ func (c *Client) memberStatusToAffiliation(memberStatus client.ChatMemberStatus) } return "member" } + +func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) { + sChatId := strconv.FormatInt(chatID, 10) + var mucJid string + if toJid != "" { + mucJid = sChatId + "@" + gateway.Jid.Bare() + } + + for i := len(messages) - 1; i >= 0; i-- { + message := messages[i] + + if plain { + reply, _ := c.getMessageReply(message) + + gateway.SendMessage( + c.jid, + sChatId, + c.formatMessage(0, 0, false, message), + strconv.FormatInt(message.Id, 10), + c.xmpp, + reply, + 0, + false, + false, + ) + } else { + c.sendMessageToGateway( + chatID, + message, + true, + mucJid + "/" + c.getMUCNickname(c.getSenderId(message)), + []string{toJid}, + ) + } + } +} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 3a2f998..679b428 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -234,6 +234,20 @@ type PresenceXMucUserStatus struct { Code uint16 `xml:"code,attr"` } +// MessageDelay is from XEP-0203 +type MessageDelay struct { + XMLName xml.Name `xml:"urn:xmpp:delay delay"` + From string `xml:"from,attr"` + Stamp string `xml:"stamp,attr"` +} + +// MessageDelayLegacy is from XEP-0203 +type MessageDelayLegacy struct { + XMLName xml.Name `xml:"jabber:x:delay x"` + From string `xml:"from,attr"` + Stamp string `xml:"stamp,attr"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -304,6 +318,16 @@ func (c PresenceXMucUserExtension) Namespace() string { return c.XMLName.Space } +// Namespace is a namespace! +func (c MessageDelay) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c MessageDelayLegacy) Namespace() string { + return c.XMLName.Space +} + // Name is a packet name func (ClientMessage) Name() string { return "message" @@ -394,4 +418,16 @@ func init() { "http://jabber.org/protocol/muc#user", "x", }, PresenceXMucUserExtension{}) + + // message delay + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:delay", + "delay", + }, MessageDelay{}) + + // legacy message delay + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "jabber:x:delay", + "x", + }, MessageDelayLegacy{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index d6b7826..074d2d0 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" "strings" "sync" + "time" "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/xmpp/extensions" @@ -42,26 +43,26 @@ 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, "", isCarbon) +func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool) { + sendMessageWrapper(to, from, body, id, component, reply, timestamp, "", isCarbon, isGroupchat) } // 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, 0, "", 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, "", false) + sendMessageWrapper(to, from, body, "", component, nil, 0, "", 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, oob, isCarbon) +func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon bool, isGroupchat bool) { + sendMessageWrapper(to, from, body, id, component, reply, timestamp, oob, isCarbon, isGroupchat) } -func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon bool, isGroupchat bool) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -76,12 +77,17 @@ func sendMessageWrapper(to string, from string, body string, id string, componen var logFrom string var messageFrom string var messageTo string - if from == "" { - logFrom = componentJid - messageFrom = componentJid - } else { + if isGroupchat { logFrom = from - messageFrom = from + "@" + componentJid + messageFrom = from + } else { + if from == "" { + logFrom = componentJid + messageFrom = componentJid + } else { + logFrom = from + messageFrom = from + "@" + componentJid + } } if isCarbon { messageTo = messageFrom @@ -95,11 +101,18 @@ func sendMessageWrapper(to string, from string, body string, id string, componen "to": to, }).Warn("Got message") + var messageType stanza.StanzaType + if isGroupchat { + messageType = stanza.MessageTypeGroupchat + } else { + messageType = stanza.MessageTypeChat + } + message := stanza.Message{ Attrs: stanza.Attrs{ From: messageFrom, To: messageTo, - Type: "chat", + Type: messageType, Id: id, }, Body: body, @@ -122,13 +135,27 @@ func sendMessageWrapper(to string, from string, body string, id string, componen if !isCarbon && toJid.Resource != "" { message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } + if timestamp != 0 { + var delayFrom string + if isGroupchat { + delayFrom, _, _ = SplitJID(from) + } + message.Extensions = append(message.Extensions, extensions.MessageDelay{ + From: delayFrom, + Stamp: time.Unix(timestamp, 0).UTC().Format(time.RFC3339), + }) + message.Extensions = append(message.Extensions, extensions.MessageDelayLegacy{ + From: delayFrom, + Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"), + }) + } if isCarbon { carbonMessage := extensions.ClientMessage{ Attrs: stanza.Attrs{ From: bareTo, To: to, - Type: "chat", + Type: messageType, }, } carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{ diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a7e3c55..bb2064a 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -440,7 +440,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence) { return } - fromBare, _, ok := gateway.SplitJID(p.From) + fromBare, fromResource, ok := gateway.SplitJID(p.From) if !ok { presenceReplySetError(reply, 400) return @@ -458,7 +458,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence) { return } - session.SendMUCStatuses(chatId) + session.JoinMUC(chatId, fromResource) } } } From e8bde731642f50c8d272c33343ec78ca70405377 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 19 Sep 2023 07:31:24 -0400 Subject: [PATCH 045/228] Original sender JID in MUCs (why?) --- telegram/handlers.go | 2 +- telegram/utils.go | 11 +++++++++-- xmpp/extensions/extensions.go | 19 +++++++++++++++++++ xmpp/gateway/gateway.go | 24 +++++++++++++++++------- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index b7277e8..8facc10 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -265,7 +265,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, 0, false, false) + gateway.SendMessage(jid, strconv.FormatInt(update.ChatId, 10), text, "e"+strconv.FormatInt(update.MessageId, 10), c.xmpp, nil, 0, false, false, "") } } } diff --git a/telegram/utils.go b/telegram/utils.go index 036956b..fd7a433 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1046,12 +1046,18 @@ func (c *Client) sendMessageToGateway(chatId int64, message *client.Message, del var isCarbon bool var jids []string var isGroupchat bool + var originalFrom string if len(groupChatTos) == 0 { isCarbon = gateway.MessageOutgoingPermissionVersion > 0 && c.Session.Carbons && message.IsOutgoing jids = c.getCarbonFullJids(isCarbon, "") } else { isGroupchat = true jids = groupChatTos + + senderId := c.getSenderId(message) + if senderId != 0 { + originalFrom = strconv.FormatInt(senderId, 10) + "@" + gateway.Jid.Full() + } } var text, oob, auxText string @@ -1138,9 +1144,9 @@ func (c *Client) sendMessageToGateway(chatId int64, message *client.Message, del } for _, jid := range jids { - gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, isCarbon, isGroupchat) + gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, isCarbon, isGroupchat, originalFrom) if auxText != "" { - gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, isCarbon, isGroupchat) + gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, isCarbon, isGroupchat, originalFrom) } } } @@ -1768,6 +1774,7 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p 0, false, false, + "", ) } else { c.sendMessageToGateway( diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 679b428..41a58fa 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -248,6 +248,19 @@ type MessageDelayLegacy struct { Stamp string `xml:"stamp,attr"` } +// MessageAddresses is from XEP-0033 +type MessageAddresses struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/address addresses"` + Addresses []MessageAddress +} + +// MessageAddress is from XEP-0033 +type MessageAddress struct { + XMLName xml.Name `xml:"address"` + Type string `xml:"type,attr"` + Jid string `xml:"jid,attr"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -430,4 +443,10 @@ func init() { "jabber:x:delay", "x", }, MessageDelayLegacy{}) + + // message addresses + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "http://jabber.org/protocol/address", + "addresses", + }, MessageAddresses{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 074d2d0..a831db6 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -43,26 +43,26 @@ 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, timestamp int64, isCarbon, isGroupchat bool) { - sendMessageWrapper(to, from, body, id, component, reply, timestamp, "", isCarbon, isGroupchat) +func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) { + sendMessageWrapper(to, from, body, id, component, reply, timestamp, "", isCarbon, isGroupchat, originalFrom) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to string, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", component, nil, 0, "", false, false) + sendMessageWrapper(to, "", body, "", component, nil, 0, "", 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, 0, "", false, false) + sendMessageWrapper(to, from, body, "", component, nil, 0, "", 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, timestamp int64, oob string, isCarbon bool, isGroupchat bool) { - sendMessageWrapper(to, from, body, id, component, reply, timestamp, oob, isCarbon, isGroupchat) +func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon bool, isGroupchat bool, originalFrom string) { + sendMessageWrapper(to, from, body, id, component, reply, timestamp, oob, isCarbon, isGroupchat, originalFrom) } -func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon bool, isGroupchat bool) { +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon bool, isGroupchat bool, originalFrom string) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -149,6 +149,16 @@ func sendMessageWrapper(to string, from string, body string, id string, componen Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"), }) } + if originalFrom != "" { + message.Extensions = append(message.Extensions, extensions.MessageAddresses{ + Addresses: []extensions.MessageAddress{ + extensions.MessageAddress{ + Type: "ofrom", + Jid: originalFrom, + }, + }, + }) + } if isCarbon { carbonMessage := extensions.ClientMessage{ From b68c07025d1dd077c73ec3b4052ea453612e3e9c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 19 Sep 2023 07:57:52 -0400 Subject: [PATCH 046/228] Add MUC history limit (maxstanzas only) --- telegram/utils.go | 4 ++-- xmpp/handlers.go | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index fd7a433..40553a4 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -304,7 +304,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o } // JoinMUC saves MUC join fact and sends initialization data -func (c *Client) JoinMUC(chatId int64, resource string) { +func (c *Client) JoinMUC(chatId int64, resource string, limit int32) { // save the nickname in this MUC, also as a marker of join c.locks.mucResourcesLock.Lock() oldMap, ok := c.mucResources[chatId] @@ -324,7 +324,7 @@ func (c *Client) JoinMUC(chatId int64, resource string) { c.sendMUCStatuses(chatId) - messages, err := c.getNLastMessages(chatId, 20) + messages, err := c.getNLastMessages(chatId, limit) if err == nil { c.sendMessagesReverse(chatId, messages, false, c.jid+"/"+resource) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index bb2064a..ff8fb21 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -292,7 +292,7 @@ func HandlePresence(s xmpp.Sender, p stanza.Packet) { var mucExt stanza.MucPresence prs.Get(&mucExt) if mucExt.XMLName.Space != "" { - handleMUCPresence(s, prs) + handleMUCPresence(s, prs, mucExt) } } @@ -403,7 +403,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { } } -func handleMUCPresence(s xmpp.Sender, p stanza.Presence) { +func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresence) { log.WithFields(log.Fields{ "type": p.Type, "from": p.From, @@ -458,7 +458,11 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence) { return } - session.JoinMUC(chatId, fromResource) + limit, ok := mucExt.History.MaxStanzas.Get() + if !ok { + limit = 20 + } + session.JoinMUC(chatId, fromResource, int32(limit)) } } } From cdaaa75c960c949a81114824b7f7b516d962ec68 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 28 Sep 2023 13:14:17 -0400 Subject: [PATCH 047/228] Send last pinned message as subject on MUC join --- telegram/utils.go | 22 ++++++++++++++++++++++ xmpp/extensions/extensions.go | 5 +++++ xmpp/gateway/gateway.go | 31 ++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 40553a4..5c27f7c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -328,6 +328,8 @@ func (c *Client) JoinMUC(chatId int64, resource string, limit int32) { if err == nil { c.sendMessagesReverse(chatId, messages, false, c.jid+"/"+resource) } + + c.sendMUCSubject(chatId, resource) } func (c *Client) sendMUCStatuses(chatID int64) { @@ -392,6 +394,26 @@ func (c *Client) sendMUCStatuses(chatID int64) { ) } +func (c *Client) sendMUCSubject(chatID int64, resource string) { + pin, err := c.client.GetChatPinnedMessage(&client.GetChatPinnedMessageRequest{ + ChatId: chatID, + }) + mucJid := strconv.FormatInt(chatID, 10) + "@" + gateway.Jid.Bare() + toJid := c.jid + "/" + resource + if err == nil { + gateway.SendSubjectMessage( + toJid, + mucJid + "/" + c.getMUCNickname(c.getSenderId(pin)), + c.messageToText(pin, false), + strconv.FormatInt(pin.Id, 10), + c.xmpp, + int64(pin.Date), + ) + } else { + gateway.SendSubjectMessage(toJid, mucJid, "", "", c.xmpp, 0) + } +} + func (c *Client) getMUCNickname(chatID int64) string { return c.formatContact(chatID) } diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 41a58fa..8ff034d 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -261,6 +261,11 @@ type MessageAddress struct { Jid string `xml:"jid,attr"` } +// EmptySubject is a dummy for MUCs to circumvent omitempty. Not registered as it would conflict with Subject field +type EmptySubject struct { + XMLName xml.Name `xml:"subject"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index a831db6..89f1eb8 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -43,26 +43,31 @@ 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, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) { - sendMessageWrapper(to, from, body, id, component, reply, timestamp, "", isCarbon, isGroupchat, originalFrom) +func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) { + sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom) } // SendServiceMessage creates and sends a simple message stanza from transport -func SendServiceMessage(to string, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", component, nil, 0, "", false, false, "") +func SendServiceMessage(to, body string, component *xmpp.Component) { + sendMessageWrapper(to, "", body, "", "", component, nil, 0, "", false, 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, 0, "", false, false, "") +func SendTextMessage(to, from, body string, component *xmpp.Component) { + sendMessageWrapper(to, from, body, "", "", component, nil, 0, "", false, 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, timestamp int64, oob string, isCarbon bool, isGroupchat bool, originalFrom string) { - sendMessageWrapper(to, from, body, id, component, reply, timestamp, oob, isCarbon, isGroupchat, originalFrom) +func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat bool, originalFrom string) { + sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom) } -func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon bool, isGroupchat bool, originalFrom string) { +// SendSubjectMessage creates and sends a MUC subject +func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { + sendMessageWrapper(to, from, "", subject, id, component, nil, timestamp, "", false, true, true, "") +} + +func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -115,7 +120,8 @@ func sendMessageWrapper(to string, from string, body string, id string, componen Type: messageType, Id: id, }, - Body: body, + Subject: subject, + Body: body, } if oob != "" { @@ -132,7 +138,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) } } - if !isCarbon && toJid.Resource != "" { + if !isGroupchat && !isCarbon && toJid.Resource != "" { message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } if timestamp != 0 { @@ -159,6 +165,9 @@ func sendMessageWrapper(to string, from string, body string, id string, componen }, }) } + if subject == "" && forceSubject { + message.Extensions = append(message.Extensions, extensions.EmptySubject{}) + } if isCarbon { carbonMessage := extensions.ClientMessage{ From 41503c7fd4e94e85a9d47f64fd8b2582cb085b37 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 28 Sep 2023 16:30:28 -0400 Subject: [PATCH 048/228] Return registration-required instead of not-authorized --- xmpp/handlers.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index ff8fb21..cfa6226 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -448,7 +448,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen session, ok := sessions[fromBare] if !ok || !session.Session.MUC { - presenceReplySetError(reply, 401) + presenceReplySetError(reply, 407) return } @@ -790,9 +790,9 @@ func presenceReplySetError(reply *stanza.Presence, code int) { case 400: reply.Error.Type = stanza.ErrorTypeModify reply.Error.Reason = "jid-malformed" - case 401: + case 407: reply.Error.Type = stanza.ErrorTypeAuth - reply.Error.Reason = "not-authorized" + reply.Error.Reason = "registration-required" case 404: reply.Error.Type = stanza.ErrorTypeCancel reply.Error.Reason = "item-not-found" From b70bb53c6d1c06f8c39806b4f71ee4e77c07657c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 29 Sep 2023 08:24:15 -0400 Subject: [PATCH 049/228] Display outgoing MUC messages --- telegram/utils.go | 52 ++++++++++++++++++++++++++++------------------- xmpp/handlers.go | 23 +++++++++++++++++---- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 5c27f7c..21c4ad3 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -362,7 +362,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { senderId = memberChat.ChatId } - nickname := c.getMUCNickname(senderId) + nickname := c.GetMUCNickname(senderId) affiliation := c.memberStatusToAffiliation(member.Status) if c.me != nil && senderId == c.me.Id { myNickname = nickname @@ -403,7 +403,7 @@ func (c *Client) sendMUCSubject(chatID int64, resource string) { if err == nil { gateway.SendSubjectMessage( toJid, - mucJid + "/" + c.getMUCNickname(c.getSenderId(pin)), + mucJid + "/" + c.GetMUCNickname(c.GetSenderId(pin)), c.messageToText(pin, false), strconv.FormatInt(pin.Id, 10), c.xmpp, @@ -414,7 +414,8 @@ func (c *Client) sendMUCSubject(chatID int64, resource string) { } } -func (c *Client) getMUCNickname(chatID int64) string { +// GetMUCNickname generates a unique nickname for a MUC member +func (c *Client) GetMUCNickname(chatID int64) string { return c.formatContact(chatID) } @@ -450,7 +451,8 @@ func (c *Client) formatContact(chatID int64) string { return str } -func (c *Client) getSenderId(message *client.Message) (senderId int64) { +// GetSenderId extracts a sender id from a message +func (c *Client) GetSenderId(message *client.Message) (senderId int64) { if message.SenderId != nil { switch message.SenderId.MessageSenderType() { case client.TypeMessageSenderUser: @@ -466,7 +468,7 @@ func (c *Client) getSenderId(message *client.Message) (senderId int64) { } func (c *Client) formatSender(message *client.Message) string { - return c.formatContact(c.getSenderId(message)) + return c.formatContact(c.GetSenderId(message)) } func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyMsg *client.Message) { @@ -486,7 +488,7 @@ func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, replyId = strconv.FormatInt(message.ReplyToMessageId, 10) } reply = &gateway.Reply{ - Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), + Author: fmt.Sprintf("%v@%s", c.GetSenderId(replyMsg), gateway.Jid.Full()), Id: replyId, } } @@ -1006,7 +1008,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } } } - if chatType != ChatTypePM || !c.Session.HideIds { + if (chatType != ChatTypePM && !c.Session.MUC) || !c.Session.HideIds { prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) } // show sender in group chats @@ -1059,12 +1061,13 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File { return file } -// ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side +// ProcessIncomingMessage is a legacy wrapper for SendMessageToGateway aiming only PM messages func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { - c.sendMessageToGateway(chatId, message, false, "", []string{}) + c.SendMessageToGateway(chatId, message, "", false, "", []string{}) } -func (c *Client) sendMessageToGateway(chatId int64, message *client.Message, delay bool, groupChatFrom string, groupChatTos []string) { +// SendMessageToGateway transfers a message to XMPP side and marks it as read on Telegram side +func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id string, delay bool, groupChatFrom string, groupChatTos []string) { var isCarbon bool var jids []string var isGroupchat bool @@ -1076,7 +1079,7 @@ func (c *Client) sendMessageToGateway(chatId int64, message *client.Message, del isGroupchat = true jids = groupChatTos - senderId := c.getSenderId(message) + senderId := c.GetSenderId(message) if senderId != 0 { originalFrom = strconv.FormatInt(senderId, 10) + "@" + gateway.Jid.Full() } @@ -1152,7 +1155,13 @@ func (c *Client) sendMessageToGateway(chatId int64, message *client.Message, del }) // forward message to XMPP - sId := strconv.FormatInt(message.Id, 10) + var sId string + if id == "" { + sId = strconv.FormatInt(message.Id, 10) + } else { + sId = id + } + var from string if groupChatFrom == "" { from = strconv.FormatInt(chatId, 10) @@ -1179,10 +1188,10 @@ 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) *client.Message { if !c.Online() { // we're offline - return 0 + return nil } if replaceId == 0 && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { @@ -1193,7 +1202,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } // do not send on success if isCommand { - return 0 + return nil } } @@ -1264,9 +1273,9 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str }) if err != nil { c.returnError(returnJid, chatID, "Not edited", err) - return 0 + return nil } - return tgMessage.Id + return tgMessage } tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ @@ -1276,9 +1285,9 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str }) if err != nil { c.returnError(returnJid, chatID, "Not sent", err) - return 0 + return nil } - return tgMessage.Id + return tgMessage } func (c *Client) returnMessage(returnJid string, chatID int64, text string) { @@ -1799,11 +1808,12 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p "", ) } else { - c.sendMessageToGateway( + c.SendMessageToGateway( chatID, message, + "", true, - mucJid + "/" + c.getMUCNickname(c.getSenderId(message)), + mucJid + "/" + c.GetMUCNickname(c.GetSenderId(message)), []string{toJid}, ) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index cfa6226..3443573 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -197,8 +197,8 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { session.SendMessageLock.Lock() defer session.SendMessageLock.Unlock() - tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) - if tgMessageId != 0 { + tgMessage := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) + if tgMessage != nil { if replaceId != 0 { // not needed (is it persistent among clients though?) /* err = gateway.IdsDB.ReplaceIdPair(session.Session.Login, bare, replace.Id, msg.Id, tgMessageId) @@ -207,9 +207,24 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } */ session.AddToOutbox(replace.Id, resource) } else { - err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) + err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessage.Id, msg.Id) if err != nil { - log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) + log.Errorf("Failed to save ids %v/%v %v", toID, tgMessage.Id, msg.Id) + } + } + + // pong groupchat messages back + if msg.Type == "groupchat" { + toJid, err := stanza.NewJid(msg.To) + if err == nil && toJid.Resource == "" { + session.SendMessageToGateway( + toID, + tgMessage, + msg.Id, + false, + msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage)), + []string{msg.From}, + ) } } } else { From a0803123b2d89c7cd9a61faeb3b2b1bcbd57dbde Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 29 Sep 2023 08:32:48 -0400 Subject: [PATCH 050/228] Advertise muc#stable_id feature --- xmpp/handlers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 3443573..0b3e71f 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -572,6 +572,7 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { "muc_unmoderated", "muc_nonanonymous", "muc_unsecured", + "http://jabber.org/protocol/muc#stable_id", ) fields := []*stanza.Field{ &stanza.Field{ @@ -594,7 +595,10 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { disco.Form = stanza.NewForm(fields, "result") } } else { - disco.AddFeatures(stanza.NSDiscoItems) + disco.AddFeatures( + stanza.NSDiscoItems, + "http://jabber.org/protocol/muc#stable_id", + ) disco.AddIdentity("Telegram group chats", "conference", "text") } } From 47fa7bca492f9d78c3bea009c7686a6bf4d8fc3b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 29 Sep 2023 16:17:25 -0400 Subject: [PATCH 051/228] Return outgoing message errors as message error stanzas (only in groupchats yet) --- telegram/utils.go | 35 ++++++++++++++++++++------------ xmpp/gateway/gateway.go | 45 +++++++++++++++++++++++++++++++++-------- xmpp/handlers.go | 5 +++-- 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 21c4ad3..eba500c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1188,7 +1188,7 @@ 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) *client.Message { +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64, isGroupchat bool) *client.Message { if !c.Online() { // we're offline return nil @@ -1198,7 +1198,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str // try to execute commands response, isCommand := c.ProcessChatCommand(chatID, text) if response != "" { - c.returnMessage(returnJid, chatID, response) + c.returnMessage(returnJid, chatID, response, 0, isGroupchat) } // do not send on success if isCommand { @@ -1224,27 +1224,31 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { response, err := http.Get(text) if err != nil { - c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err) + c.returnError(returnJid, chatID, "Failed to fetch the uploaded file", err, 500, isGroupchat) } if response != nil && response.Body != nil { defer response.Body.Close() if response.StatusCode != 200 { - c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode)) + c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode), response.StatusCode, isGroupchat) + return nil } tempDir, err := ioutil.TempDir("", "telegabber-*") if err != nil { - c.returnError(returnJid, chatID, "Failed to create a temporary directory", err) + c.returnError(returnJid, chatID, "Failed to create a temporary directory", err, 500, isGroupchat) + return nil } tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text))) if err != nil { - c.returnError(returnJid, chatID, "Failed to create a temporary file", err) + c.returnError(returnJid, chatID, "Failed to create a temporary file", err, 500, isGroupchat) + return nil } _, err = io.Copy(tempFile, response.Body) if err != nil { - c.returnError(returnJid, chatID, "Failed to write a temporary file", err) + c.returnError(returnJid, chatID, "Failed to write a temporary file", err, 500, isGroupchat) + return nil } file = &client.InputFileLocal{ @@ -1272,7 +1276,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str InputMessageContent: content, }) if err != nil { - c.returnError(returnJid, chatID, "Not edited", err) + c.returnError(returnJid, chatID, "Not edited", err, 400, isGroupchat) return nil } return tgMessage @@ -1284,18 +1288,23 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str InputMessageContent: content, }) if err != nil { - c.returnError(returnJid, chatID, "Not sent", err) + c.returnError(returnJid, chatID, "Not sent", err, 400, isGroupchat) return nil } return tgMessage } -func (c *Client) returnMessage(returnJid string, chatID int64, text string) { - gateway.SendTextMessage(returnJid, strconv.FormatInt(chatID, 10), text, c.xmpp) +func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) { + sChatId := strconv.FormatInt(chatID, 10) + if isGroupchat { + gateway.SendErrorMessage(returnJid, sChatId + "@" + gateway.Jid.Bare(), text, code, isGroupchat, c.xmpp) + } else { + gateway.SendTextMessage(returnJid, sChatId, text, c.xmpp) + } } -func (c *Client) returnError(returnJid string, chatID int64, msg string, err error) { - c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error())) +func (c *Client) returnError(returnJid string, chatID int64, msg string, err error, code int, isGroupchat bool) { + c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error()), code, isGroupchat) } func (c *Client) prepareOutgoingMessageContent(text string, file *client.InputFileLocal) client.InputMessageContent { diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 89f1eb8..9a42077 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -44,30 +44,35 @@ var MessageOutgoingPermissionVersion = 0 // SendMessage creates and sends a message stanza func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom) + sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom, 0) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", "", component, nil, 0, "", false, false, false, "") + sendMessageWrapper(to, "", body, "", "", component, nil, 0, "", false, false, false, "", 0) } // SendTextMessage creates and sends a simple message stanza func SendTextMessage(to, from, body string, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", "", component, nil, 0, "", false, false, false, "") + sendMessageWrapper(to, from, body, "", "", component, nil, 0, "", false, false, false, "", 0) +} + +// SendErrorMessage creates and sends an error message stanza +func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { + sendMessageWrapper(to, from, text, "", "", component, nil, 0, "", false, isGroupchat, false, "", code) } // SendMessageWithOOB creates and sends a message stanza with OOB URL func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom) + sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom, 0) } // SendSubjectMessage creates and sends a MUC subject func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { - sendMessageWrapper(to, from, "", subject, id, component, nil, timestamp, "", false, true, true, "") + sendMessageWrapper(to, from, "", subject, id, component, nil, timestamp, "", false, true, true, "", 0) } -func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string) { +func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string, errorCode int) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -107,7 +112,9 @@ func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Comp }).Warn("Got message") var messageType stanza.StanzaType - if isGroupchat { + if errorCode != 0 { + messageType = stanza.MessageTypeError + } else if isGroupchat { messageType = stanza.MessageTypeGroupchat } else { messageType = stanza.MessageTypeChat @@ -121,7 +128,29 @@ func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Comp Id: id, }, Subject: subject, - Body: body, + } + if errorCode == 0 { + message.Body = body + } else { + message.Error = stanza.Err{ + Code: errorCode, + Text: body, + } + switch errorCode { + case 400: + message.Error.Type = stanza.ErrorTypeModify + message.Error.Reason = "bad-request" + case 404: + message.Error.Type = stanza.ErrorTypeCancel + message.Error.Reason = "item-not-found" + case 500: + message.Error.Type = stanza.ErrorTypeWait + message.Error.Reason = "internal-server-error" + default: + log.Error("Unknown error code, falling back with empty reason") + message.Error.Type = stanza.ErrorTypeCancel + message.Error.Reason = "undefined-condition" + } } if oob != "" { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 0b3e71f..72b46eb 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -194,10 +194,11 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { return } } + isGroupchat := msg.Type == "groupchat" session.SendMessageLock.Lock() defer session.SendMessageLock.Unlock() - tgMessage := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) + tgMessage := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, isGroupchat) if tgMessage != nil { if replaceId != 0 { // not needed (is it persistent among clients though?) @@ -214,7 +215,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } // pong groupchat messages back - if msg.Type == "groupchat" { + if isGroupchat { toJid, err := stanza.NewJid(msg.To) if err == nil && toJid.Resource == "" { session.SendMessageToGateway( From 02578440cd02ceb6716f17acd9a77288bda1a561 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 29 Sep 2023 16:59:13 -0400 Subject: [PATCH 052/228] Detect the "Have no write access to the chat" error from Telegram --- telegram/utils.go | 7 +++++++ xmpp/gateway/gateway.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index eba500c..cab450f 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1304,6 +1304,13 @@ func (c *Client) returnMessage(returnJid string, chatID int64, text string, code } func (c *Client) returnError(returnJid string, chatID int64, msg string, err error, code int, isGroupchat bool) { + responseError, ok := err.(client.ResponseError) + log.Debugf("responseError: %#v", responseError) + if ok && responseError.Err != nil { + if responseError.Err.Message == "Have no write access to the chat" { + code = 403 + } + } c.returnMessage(returnJid, chatID, fmt.Sprintf("%s: %s", msg, err.Error()), code, isGroupchat) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 9a42077..09cd42f 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -140,6 +140,9 @@ func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Comp case 400: message.Error.Type = stanza.ErrorTypeModify message.Error.Reason = "bad-request" + case 403: + message.Error.Type = stanza.ErrorTypeAuth + message.Error.Reason = "forbidden" case 404: message.Error.Type = stanza.ErrorTypeCancel message.Error.Reason = "item-not-found" From b8a57c06b646edbad340c9e1c6c79e11a8a666e3 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 30 Sep 2023 06:29:40 -0400 Subject: [PATCH 053/228] Handle MUC PM attempts --- xmpp/gateway/gateway.go | 29 ++++++++++++++++----------- xmpp/handlers.go | 43 +++++++++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 09cd42f..1a37cc7 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -44,35 +44,40 @@ var MessageOutgoingPermissionVersion = 0 // SendMessage creates and sends a message stanza func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom, 0) + sendMessageWrapper(to, from, body, "", "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom, 0) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", "", component, nil, 0, "", false, false, false, "", 0) + sendMessageWrapper(to, "", body, "", "", "", component, nil, 0, "", false, false, false, "", 0) } // SendTextMessage creates and sends a simple message stanza func SendTextMessage(to, from, body string, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", "", component, nil, 0, "", false, false, false, "", 0) + sendMessageWrapper(to, from, body, "", "", "", component, nil, 0, "", false, false, false, "", 0) } // SendErrorMessage creates and sends an error message stanza func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, text, "", "", component, nil, 0, "", false, isGroupchat, false, "", code) + sendMessageWrapper(to, from, "", "", text, "", component, nil, 0, "", false, isGroupchat, false, "", code) +} + +// SendErrorMessageWithBody creates and sends an error message stanza with body payload +func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) { + sendMessageWrapper(to, from, body, "", errorText, id, component, nil, 0, "", false, isGroupchat, false, "", code) } // SendMessageWithOOB creates and sends a message stanza with OOB URL func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom, 0) + sendMessageWrapper(to, from, body, "", "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom, 0) } // SendSubjectMessage creates and sends a MUC subject func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { - sendMessageWrapper(to, from, "", subject, id, component, nil, timestamp, "", false, true, true, "", 0) + sendMessageWrapper(to, from, "", subject, "", id, component, nil, timestamp, "", false, true, true, "", 0) } -func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string, errorCode int) { +func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string, errorCode int) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -128,13 +133,12 @@ func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Comp Id: id, }, Subject: subject, + Body: body, } - if errorCode == 0 { - message.Body = body - } else { + if errorCode != 0 { message.Error = stanza.Err{ Code: errorCode, - Text: body, + Text: errorText, } switch errorCode { case 400: @@ -146,6 +150,9 @@ func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Comp case 404: message.Error.Type = stanza.ErrorTypeCancel message.Error.Reason = "item-not-found" + case 406: + message.Error.Type = stanza.ErrorTypeModify + message.Error.Reason = "not-acceptable" case 500: message.Error.Type = stanza.ErrorTypeWait message.Error.Reason = "internal-server-error" diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 72b46eb..3ab79c7 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -123,6 +123,26 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { toID, ok := toToID(msg.To) if ok { + toJid, err := stanza.NewJid(msg.To) + if err != nil { + log.Error("Invalid to JID!") + return + } + + isGroupchat := msg.Type == "groupchat" + + if session.Session.MUC && toJid.Resource != "" { + chat, _, err := session.GetContactByID(toID, nil) + if err == nil && session.IsGroup(chat) { + if isGroupchat { + gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "", msg.Id, 400, true, component) + } else { + gateway.SendErrorMessage(msg.From, msg.To, "PMing room members is not supported, use the real JID", 406, true, component) + } + return + } + } + var reply extensions.Reply var fallback extensions.Fallback var replace extensions.Replace @@ -134,7 +154,6 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { log.Debugf("replace: %#v", replace) var replyId int64 - var err error text := msg.Body if len(reply.Id) > 0 { chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, reply.Id) @@ -194,7 +213,6 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { return } } - isGroupchat := msg.Type == "groupchat" session.SendMessageLock.Lock() defer session.SendMessageLock.Unlock() @@ -215,18 +233,15 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } // pong groupchat messages back - if isGroupchat { - toJid, err := stanza.NewJid(msg.To) - if err == nil && toJid.Resource == "" { - session.SendMessageToGateway( - toID, - tgMessage, - msg.Id, - false, - msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage)), - []string{msg.From}, - ) - } + if isGroupchat && toJid.Resource == "" { + session.SendMessageToGateway( + toID, + tgMessage, + msg.Id, + false, + msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage)), + []string{msg.From}, + ) } } else { /* From 1e7e761c6ce7bda3850ed23d0e64d884a28f60a7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 3 Oct 2023 18:56:37 -0400 Subject: [PATCH 054/228] Reflect name change of Telegram user in all MUCs --- telegram/client.go | 25 +++++++-- telegram/handlers.go | 7 +++ telegram/utils.go | 95 ++++++++++++++++++++++++++++------- xmpp/extensions/extensions.go | 1 + xmpp/gateway/gateway.go | 6 +++ 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 6033c7c..f3bfe4a 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -41,6 +41,25 @@ type DelayedStatus struct { TimestampExpired int64 } +// MUCState holds MUC metadata +type MUCState struct { + Resources map[string]bool + Members map[int64]*MUCMember +} + +// MUCMember represents a MUC member +type MUCMember struct { + Nickname string + Affiliation string +} + +func NewMUCState() *MUCState { + return &MUCState{ + Resources: make(map[string]bool), + Members: make(map[int64]*MUCMember), + } +} + // Client stores the metadata for lazily invoked TDlib instance type Client struct { client *client.Client @@ -64,7 +83,7 @@ type Client struct { lastMsgHashes map[int64]uint64 msgHashSeed maphash.Seed - mucResources map[int64]map[string]bool + mucCache map[int64]*MUCState locks clientLocks SendMessageLock sync.Mutex @@ -75,7 +94,7 @@ type clientLocks struct { chatMessageLocks map[int64]*sync.Mutex resourcesLock sync.Mutex outboxLock sync.Mutex - mucResourcesLock sync.Mutex + mucCacheLock sync.Mutex lastMsgHashesLock sync.Mutex authorizerReadLock sync.Mutex @@ -136,7 +155,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component Session: session, resources: make(map[string]bool), outbox: make(map[string]string), - mucResources: make(map[int64]map[string]bool), + mucCache: make(map[int64]*MUCState), content: &conf.Content, cache: cache.NewCache(), options: options, diff --git a/telegram/handlers.go b/telegram/handlers.go index 8facc10..c7185f5 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -153,6 +153,13 @@ func (c *Client) updateHandler() { // new user discovered func (c *Client) updateUser(update *client.UpdateUser) { + // check if MUC nicknames should be updated + cacheUser, ok := c.cache.GetUser(update.User.Id) + if ok && (cacheUser.FirstName != update.User.FirstName || cacheUser.LastName != update.User.LastName) { + newNickname := c.GetMUCNickname(update.User.Id) + c.updateMUCsNickname(update.User.Id, newNickname) + } + c.cache.SetUser(update.User.Id, update.User) show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id) go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType)) diff --git a/telegram/utils.go b/telegram/utils.go index cab450f..addeee8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -306,21 +306,19 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o // JoinMUC saves MUC join fact and sends initialization data func (c *Client) JoinMUC(chatId int64, resource string, limit int32) { // save the nickname in this MUC, also as a marker of join - c.locks.mucResourcesLock.Lock() - oldMap, ok := c.mucResources[chatId] - if ok { - _, ok := oldMap[resource] - if ok { - // already joined, initializing anyway - } else { - oldMap[resource] = true - } - } else { - newMap := make(map[string]bool) - newMap[resource] = true - c.mucResources[chatId] = newMap + c.locks.mucCacheLock.Lock() + mucState, ok := c.mucCache[chatId] + if !ok || mucState == nil { + mucState = NewMUCState() + c.mucCache[chatId] = mucState } - c.locks.mucResourcesLock.Unlock() + _, ok = mucState.Resources[resource] + if ok { + // already joined, initializing anyway + } else { + mucState.Resources[resource] = true + } + c.locks.mucCacheLock.Unlock() c.sendMUCStatuses(chatId) @@ -332,14 +330,27 @@ func (c *Client) JoinMUC(chatId int64, resource string, limit int32) { c.sendMUCSubject(chatId, resource) } +func (c *Client) getFullName(user *client.User) string { + fullName := user.FirstName + if user.LastName != "" { + fullName = fullName + " " + user.LastName + } + return fullName +} + func (c *Client) sendMUCStatuses(chatID int64) { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + mucState, ok := c.mucCache[chatID] + if !ok || mucState == nil { + mucState = NewMUCState() + c.mucCache[chatID] = mucState + } + sChatId := strconv.FormatInt(chatID, 10) myNickname := "me" if c.me != nil { - myNickname := c.me.FirstName - if c.me.LastName != "" { - myNickname = myNickname + " " + c.me.LastName - } + myNickname = c.getFullName(c.me) } myAffiliation := "member" @@ -364,6 +375,11 @@ func (c *Client) sendMUCStatuses(chatID int64) { nickname := c.GetMUCNickname(senderId) affiliation := c.memberStatusToAffiliation(member.Status) + mucState.Members[senderId] = &MUCMember{ + Nickname: nickname, + Affiliation: affiliation, + } + if c.me != nil && senderId == c.me.Id { myNickname = nickname myAffiliation = affiliation @@ -419,6 +435,49 @@ func (c *Client) GetMUCNickname(chatID int64) string { return c.formatContact(chatID) } +func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + for mucId, state := range c.mucCache { + oldMember, ok := state.Members[memberID] + if ok { + state.Members[memberID] = &MUCMember{ + Nickname: newNickname, + Affiliation: oldMember.Affiliation, + } + + sMucId := strconv.FormatInt(mucId, 10) + unavailableStatusCodes := []uint16{303, 210} + availableStatusCodes := []uint16{100, 210} + if c.me != nil && memberID == c.me.Id { + unavailableStatusCodes = append(unavailableStatusCodes, 110) + availableStatusCodes = append(availableStatusCodes, 110) + } + gateway.SendPresence( + c.xmpp, + c.jid, + gateway.SPType("unavailable"), + gateway.SPFrom(sMucId), + gateway.SPResource(oldMember.Nickname), + gateway.SPImmed(true), + gateway.SPMUCAffiliation(oldMember.Affiliation), + gateway.SPMUCNick(newNickname), + gateway.SPMUCStatusCodes(unavailableStatusCodes), + ) + gateway.SendPresence( + c.xmpp, + c.jid, + gateway.SPFrom(sMucId), + gateway.SPResource(newNickname), + gateway.SPImmed(true), + gateway.SPMUCAffiliation(oldMember.Affiliation), + gateway.SPMUCStatusCodes(availableStatusCodes), + ) + } + } +} + func (c *Client) formatContact(chatID int64) string { if chatID == 0 { return "" diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 8ff034d..45ab839 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -225,6 +225,7 @@ type PresenceXMucUserItem struct { XMLName xml.Name `xml:"item"` Affiliation string `xml:"affiliation,attr"` Jid string `xml:"jid,attr"` + Nick string `xml:"nick,attr,omitempty"` Role string `xml:"role,attr"` } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 1a37cc7..9007f6b 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -328,6 +328,9 @@ var SPImmed = args.NewBool(args.Default(true)) // SPMUCAffiliation is a XEP-0045 MUC affiliation var SPMUCAffiliation = args.NewString() +// SPMUCNick is a XEP-0045 MUC user nick +var SPMUCNick = args.NewString() + // SPMUCJid is a real jid of a MUC member var SPMUCJid = args.NewString() @@ -398,6 +401,9 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { Role: affilationToRole(affiliation), }, } + if SPMUCNick.IsSet(args) { + userExt.Item.Nick = SPMUCNick.Get(args) + } if SPMUCJid.IsSet(args) { userExt.Item.Jid = SPMUCJid.Get(args) } From 4972cb6d5e68c0ad9f7b290e33f49ecf6436361b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 4 Oct 2023 05:57:29 -0400 Subject: [PATCH 055/228] Reject MUC nickname change attempts --- telegram/utils.go | 33 +++++++++++++++++++++ xmpp/handlers.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index addeee8..a730113 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -478,6 +478,39 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { } } +// MUCHasResource checks if a MUC was joined from a given resource +func (c *Client) MUCHasResource(chatID int64, resource string) bool { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + mucState, ok := c.mucCache[chatID] + if !ok || mucState == nil { + return false + } + _, ok = mucState.Resources[resource] + return ok +} + +// GetMyMUCNickname obtains this account's nickname in a given MUC +func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { + if c.me == nil { + return "", false + } + + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + mucState, ok := c.mucCache[chatID] + if !ok || mucState == nil { + return "", false + } + member, ok := mucState.Members[c.me.Id] + if !ok { + return "", false + } + return member.Nickname, true +} + func (c *Client) formatContact(chatID int64) string { if chatID == 0 { return "" diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 3ab79c7..eea14be 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -324,7 +324,9 @@ func HandlePresence(s xmpp.Sender, p stanza.Packet) { prs.Get(&mucExt) if mucExt.XMLName.Space != "" { handleMUCPresence(s, prs, mucExt) + return } + tryHandleMUCNicknameChange(s, prs) } func handleSubscription(s xmpp.Sender, p stanza.Presence) { @@ -498,6 +500,77 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen } } +func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) { + log.WithFields(log.Fields{ + "type": p.Type, + "from": p.From, + "to": p.To, + }).Warn("Nickname change presence?") + log.Debugf("%#v", p) + + if p.Type != "" { + return + } + + toBare, nickname, ok := gateway.SplitJID(p.To) + if !ok || nickname == "" { + return + } + + fromBare, fromResource, ok := gateway.SplitJID(p.From) + if !ok { + return + } + + session, ok := sessions[fromBare] + if !ok || !session.Session.MUC { + return + } + + chatId, ok := toToID(toBare) + if !ok { + return + } + + chat, _, err := session.GetContactByID(chatId, nil) + if err != nil || !session.IsGroup(chat) { + return + } + + if !session.MUCHasResource(chatId, fromResource) { + return + } + + log.Warn("🗿 Yes") + + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + from := toBare + nickname, ok = session.GetMyMUCNickname(chatId) + if ok { + from = from+"/"+nickname + } + reply := &stanza.Presence{ + Attrs: stanza.Attrs{ + From: from, + To: p.From, + Id: p.Id, + Type: stanza.PresenceTypeError, + }, + Error: stanza.Err{ + Code: 406, + Type: stanza.ErrorTypeModify, + Reason: "not-acceptable", + Text: "Telegram does not support changing nicknames per-chat. Issue a /setname command to the transport if you wish to change the global name", + }, + } + gateway.ResumableSend(component, reply) +} + func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { log.WithFields(log.Fields{ "from": iq.From, From 67b8ad57f0aab1a033cbd8bcc8e75cf23342f5f9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 29 Oct 2023 08:47:35 -0400 Subject: [PATCH 056/228] 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 057/228] 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 058/228] 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 059/228] 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 060/228] 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 061/228] 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 062/228] 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 063/228] 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 064/228] 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 065/228] 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 066/228] 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 067/228] 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 068/228] 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 069/228] 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 070/228] 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 071/228] 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 072/228] 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 073/228] 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 074/228] 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 075/228] 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 076/228] 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 077/228] 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 078/228] 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 079/228] 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 080/228] 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 081/228] 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 082/228] 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 083/228] 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 084/228] 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 085/228] 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 086/228] 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 087/228] 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 088/228] 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 089/228] 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 090/228] 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 091/228] 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 092/228] 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 093/228] 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 094/228] 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 095/228] 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 096/228] 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 097/228] 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 098/228] 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 099/228] 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 100/228] 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 101/228] 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 102/228] 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 103/228] 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 104/228] 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 105/228] 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 106/228] 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 107/228] 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 108/228] 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 109/228] 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 { From 68c3bece71beb55e4b3b882ba245c5b58d003745 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 5 May 2025 17:54:56 -0400 Subject: [PATCH 110/228] Apply hideids to carbons in groupchats --- Makefile | 2 +- telegabber.go | 2 +- telegram/utils.go | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 8a3b062..80607e2 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.0" +VERSION := "v1.12.1" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 1f0f3e0..7d9a329 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.0" +var version string = "1.12.1" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index b0d1dd1..789f635 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1110,7 +1110,8 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } } } - if !isPM || !c.Session.HideIds { + // with hideids options enabled, hide the id for everything but non-carbons in group chats + if (!isPM && !(c.isCarbonsEnabled() && message.IsOutgoing)) || !c.Session.HideIds { prefix = append(prefix, directionChar+strconv.FormatInt(message.Id, 10)) } // show sender in group chats From 71f59ee73974ed9a6e13f148a9c743e31ff13775 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 8 May 2025 20:17:16 -0400 Subject: [PATCH 111/228] Duplicate chat presences as following MUC member presences too --- telegram/utils.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index fe9591e..05c9f05 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -454,11 +454,32 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o gateway.SPPhoto(photo), gateway.SPImmed(gateway.SPImmed.Get(oldArgs)), } - newArgs = gateway.SPAppendFrom(newArgs, chatID) if presenceType != "" { newArgs = append(newArgs, gateway.SPType(presenceType)) } + c.locks.mucCacheLock.Lock() + for mucId, state := range c.mucCache { + member, ok := state.Members[chatID] + if ok { + sMucId := strconv.FormatInt(mucId, 10) + newMucArgs := append( + newArgs, + gateway.SPFrom(sMucId), + gateway.SPResource(member.Nickname), + gateway.SPMUCAffiliation(member.Affiliation), + ) + err := c.sendPresence(newMucArgs...) + if err != nil { + c.locks.mucCacheLock.Unlock() + return err + } + } + } + c.locks.mucCacheLock.Unlock() + + newArgs = gateway.SPAppendFrom(newArgs, chatID) + return c.sendPresence(newArgs...) } From 48cd525b8e17fe133fbba9001146b33e80bbdb5e Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 8 May 2025 20:17:42 -0400 Subject: [PATCH 112/228] c.sendPresence: exists --- telegram/utils.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 05c9f05..75b5fff 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -566,9 +566,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { continue } - gateway.SendPresence( - c.xmpp, - c.jid, + c.sendPresence( gateway.SPFrom(sChatId), gateway.SPResource(nickname), gateway.SPImmed(true), @@ -579,9 +577,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { } // according to the spec, own member entry should be sent the last - gateway.SendPresence( - c.xmpp, - c.jid, + c.sendPresence( gateway.SPFrom(sChatId), gateway.SPResource(myNickname), gateway.SPImmed(true), @@ -634,9 +630,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { unavailableStatusCodes = append(unavailableStatusCodes, 110) availableStatusCodes = append(availableStatusCodes, 110) } - gateway.SendPresence( - c.xmpp, - c.jid, + c.sendPresence( gateway.SPType("unavailable"), gateway.SPFrom(sMucId), gateway.SPResource(oldMember.Nickname), @@ -645,9 +639,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPMUCNick(newNickname), gateway.SPMUCStatusCodes(unavailableStatusCodes), ) - gateway.SendPresence( - c.xmpp, - c.jid, + c.sendPresence( gateway.SPFrom(sMucId), gateway.SPResource(newNickname), gateway.SPImmed(true), From 6a8a96a4f4f8bc061e918f5ccfffe6000b02f9b9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 8 May 2025 20:59:28 -0400 Subject: [PATCH 113/228] Send real JID in more of presences --- telegram/utils.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index 75b5fff..5ea904a 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -459,6 +459,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o } c.locks.mucCacheLock.Lock() + chatJid := strconv.FormatInt(chatID, 10) + "@" + gateway.Jid.Full() for mucId, state := range c.mucCache { member, ok := state.Members[chatID] if ok { @@ -468,6 +469,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o gateway.SPFrom(sMucId), gateway.SPResource(member.Nickname), gateway.SPMUCAffiliation(member.Affiliation), + gateway.SPMUCJid(chatJid), ) err := c.sendPresence(newMucArgs...) if err != nil { @@ -615,6 +617,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() + realJid := strconv.FormatInt(memberID, 10) + "@" + gateway.Jid.Full() for mucId, state := range c.mucCache { oldMember, ok := state.Members[memberID] if ok { @@ -638,6 +641,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPMUCAffiliation(oldMember.Affiliation), gateway.SPMUCNick(newNickname), gateway.SPMUCStatusCodes(unavailableStatusCodes), + gateway.SPMUCJid(realJid), ) c.sendPresence( gateway.SPFrom(sMucId), @@ -645,6 +649,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPImmed(true), gateway.SPMUCAffiliation(oldMember.Affiliation), gateway.SPMUCStatusCodes(availableStatusCodes), + gateway.SPMUCJid(realJid), ) } } From 0500b3535f10b54f4a39b2a2a8f951b2acc491b4 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 8 May 2025 21:53:53 -0400 Subject: [PATCH 114/228] Less "Nickname change presence?" warnings --- xmpp/handlers.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 5f3f9ff..1db3a41 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -536,13 +536,6 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen } func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) { - log.WithFields(log.Fields{ - "type": p.Type, - "from": p.From, - "to": p.To, - }).Warn("Nickname change presence?") - log.Debugf("%#v", p) - if p.Type != "" { return } @@ -552,6 +545,13 @@ func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) { return } + log.WithFields(log.Fields{ + "type": p.Type, + "from": p.From, + "to": p.To, + }).Warn("Nickname change presence?") + log.Debugf("%#v", p) + fromBare, fromResource, ok := gateway.SplitJID(p.From) if !ok { return From a05724b93aaf7a422b86dab2d78f42d571d40ab8 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 9 May 2025 00:07:55 -0400 Subject: [PATCH 115/228] Add legacy-to-MUC migrator --- telegram/commands.go | 3 +++ telegram/utils.go | 10 +++++++++ xmpp/extensions/extensions.go | 27 +++++++++++++++++++++++ xmpp/gateway/gateway.go | 40 ++++++++++++++++++++++++++--------- xmpp/handlers.go | 3 +++ 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index cda84cb..ef73c37 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -463,6 +463,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin if err != nil { return err.Error(), false } + if args[0] == "muc" && args[1] == "true" { + go c.MigrateToMUCs() + } gateway.DirtySessions = true return fmt.Sprintf("%s set to %s", args[0], value), true diff --git a/telegram/utils.go b/telegram/utils.go index 5ea904a..b158c71 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2351,3 +2351,13 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member } return members, nil } + +// MigrateToMUCs unsubscribes from legacy group chats and invites to MUCs +func (c *Client) MigrateToMUCs() { + for _, chat := range c.GetGroupChats() { + c.unsubscribe(chat.Id) + for resource := range c.resourcesRange() { + gateway.InviteToMUC(chat.Id, c.jid+"/"+resource, c.xmpp) + } + } +} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 45ab839..d40b5f6 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -213,6 +213,27 @@ type QueryRegisterRemove struct { XMLName xml.Name `xml:"remove"` } +// MessageXMucUserExtension is from XEP-0045 +type MessageXMucUserExtension struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` + Invite MessageXMucUserInvite + Password string `xml:"password,omitempty"` +} + +// MessageXMucUserInvite is from XEP-0045 +type MessageXMucUserInvite struct { + XMLName xml.Name `xml:"invite"` + From string `xml:"from,attr"` + Reason string `xml:"reason,omitempty"` + Continue MessageXMucUserInviteContinue +} + +// MessageXMucUserInviteContinue is from XEP-0045 +type MessageXMucUserInviteContinue struct { + XMLName xml.Name `xml:"continue"` + Thread string `xml:"thread,attr,omitempty"` +} + // PresenceXMucUserExtension is from XEP-0045 type PresenceXMucUserExtension struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` @@ -432,6 +453,12 @@ func init() { "query", }, QueryRegister{}) + // message muc user + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "http://jabber.org/protocol/muc#user", + "x", + }, MessageXMucUserExtension{}) + // presence muc user stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{ "http://jabber.org/protocol/muc#user", diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 8bf3302..65d488a 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -63,7 +63,7 @@ var MessageOutgoingPermissionVersion = 0 // SendMessage creates and sends a message stanza func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0) + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "") } // SendServiceMessage creates and sends a simple message stanza from transport @@ -72,7 +72,7 @@ func SendServiceMessage(to, body string, component *xmpp.Component) { if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0) + sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "") } // SendTextMessage creates and sends a simple message stanza @@ -81,27 +81,27 @@ func SendTextMessage(to, from, body string, component *xmpp.Component) { if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0) + sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "") } // SendErrorMessage creates and sends an error message stanza func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code) + sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "") } // SendErrorMessageWithBody creates and sends an error message stanza with body payload func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code) + sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "") } // SendMessageWithOOB creates and sends a message stanza with OOB URL func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0) + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "") } // SendSubjectMessage creates and sends a MUC subject func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { - sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0) + sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "") } // SendMessageMarker creates and sends a message stanza with a XEP-0333 marker @@ -109,10 +109,15 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker sendMessageWrapper(to, from, "", "", "", "", component, nil, &marker{ Type: markerType, Id: markerId, - }, 0, "", "", false, false, false, false, "", 0) + }, 0, "", "", false, false, false, false, "", 0, "") } -func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int) { +// SendMUCInvite creates and send a MUC invitation message +func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) { + sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom) +} + +func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom string) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -134,6 +139,9 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if from == "" { logFrom = componentJid messageFrom = componentJid + } else if inviteFrom != "" { + logFrom = from + messageFrom = from + "@" + Jid.Bare() } else { logFrom = from messageFrom = from + "@" + componentJid @@ -212,7 +220,7 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) } } - if !isGroupchat && !isCarbon && toJid.Resource != "" { + if !isGroupchat && !isCarbon && toJid.Resource != "" && inviteFrom == "" { message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } if timestamp != 0 { @@ -256,6 +264,13 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if replaceId != "" { message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId}) } + if inviteFrom != "" { + message.Extensions = append(message.Extensions, extensions.MessageXMucUserExtension{ + Invite: extensions.MessageXMucUserInvite{ + From: inviteFrom, + }, + }) + } if isCarbon { carbonMessage := extensions.ClientMessage{ @@ -616,3 +631,8 @@ func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId _ = ResumableSend(component, message) } + +func InviteToMUC(chatID int64, jid string, component *xmpp.Component) { + sChatID := strconv.FormatInt(chatID, 10) + SendMUCInvite(jid, sChatID, component, Jid.Full()) +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1db3a41..2f3cb0a 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1137,6 +1137,9 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command errString = fmt.Sprintf("Error for field %v: %v, aborting", field.Var, err.Error()) break } + if field.Var == "muc" && fieldValue == "true" { + go session.MigrateToMUCs() + } infoStrings = append(infoStrings, fmt.Sprintf("%s set to %s", field.Var, value)) gateway.DirtySessions = true } From 76919e0c9679cdf68e2dc3f58c69f90c9a845356 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 9 May 2025 22:17:28 -0400 Subject: [PATCH 116/228] Duplicate MUC invitations via a legacy element --- xmpp/extensions/extensions.go | 6 ++++++ xmpp/gateway/gateway.go | 2 ++ 2 files changed, 8 insertions(+) diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index d40b5f6..2afc36c 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -213,6 +213,12 @@ type QueryRegisterRemove struct { XMLName xml.Name `xml:"remove"` } +// MessageXLegacyInviteExtension is from JEP-0045 +type MessageXLegacyInviteExtension struct { + XMLName xml.Name `xml:"jabber:x:conference x"` + Jid string `xml:"jid,attr"` +} + // MessageXMucUserExtension is from XEP-0045 type MessageXMucUserExtension struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 65d488a..28147eb 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -269,6 +269,8 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component Invite: extensions.MessageXMucUserInvite{ From: inviteFrom, }, + }, extensions.MessageXLegacyInviteExtension{ + Jid: messageFrom, }) } From a5a3fdf688cacac0f9d0f397af13cf5078de5dc0 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 9 May 2025 22:32:17 -0400 Subject: [PATCH 117/228] Purge delayed statuses for legacy groups before migration --- telegram/utils.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index b158c71..ee3bc8b 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2354,10 +2354,21 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member // MigrateToMUCs unsubscribes from legacy group chats and invites to MUCs func (c *Client) MigrateToMUCs() { + var chatIDs []int64 for _, chat := range c.GetGroupChats() { - c.unsubscribe(chat.Id) + chatIDs = append(chatIDs, chat.Id) + } + + c.DelayedStatusesLock.Lock() + for _, chatID := range chatIDs { + delete(c.DelayedStatuses, chatID) + } + c.DelayedStatusesLock.Unlock() + + for _, chatID := range chatIDs { + c.unsubscribe(chatID) for resource := range c.resourcesRange() { - gateway.InviteToMUC(chat.Id, c.jid+"/"+resource, c.xmpp) + gateway.InviteToMUC(chatID, c.jid+"/"+resource, c.xmpp) } } } From b0be2ad259a560e72b2d4484e64b545c015f7134 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 11 May 2025 02:39:06 -0400 Subject: [PATCH 118/228] Set message type=normal for invites for Gajim --- xmpp/gateway/gateway.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 28147eb..990ba42 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -164,6 +164,8 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component messageType = stanza.MessageTypeError } else if isGroupchat { messageType = stanza.MessageTypeGroupchat + } else if inviteFrom != "" { + messageType = stanza.MessageTypeNormal } else { messageType = stanza.MessageTypeChat } From f123f6090595510ce36c43712ffb976d7d296beb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 12 May 2025 06:34:00 -0400 Subject: [PATCH 119/228] Move MUCs to separate c-prefixed JIDs --- telegram/handlers.go | 18 ++++++++-- telegram/utils.go | 34 ++++++++---------- xmpp/gateway/gateway.go | 37 +++++++++++++++---- xmpp/handlers.go | 78 ++++++++++++++++++++++++++--------------- 4 files changed, 110 insertions(+), 57 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 1ce1636..021280d 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -73,7 +73,7 @@ func (c *Client) sendMarker(chatId, messageId int64, typ gateway.MarkerType) { gateway.SendMessageMarker( c.jid, - strconv.FormatInt(chatId, 10), + gateway.CHATNODE(chatId), c.xmpp, typ, xmppId, @@ -390,6 +390,12 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { } } + var isGroupchat bool + chat, _, _ := c.GetContactByID(update.ChatId, nil) + if c.Session.MUC && c.IsGroup(chat) { + isGroupchat = true + } + var deleteChar string if c.Session.AsciiArrows { deleteChar = "X " @@ -397,7 +403,13 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { deleteChar = "✗ " } text := deleteChar + strings.Join(int64SliceToStringSlice(update.MessageIds), ",") - gateway.SendTextMessage(c.jid, strconv.FormatInt(update.ChatId, 10), text, c.xmpp) + var fromJid string + if isGroupchat { + fromJid = gateway.MUCJID(update.ChatId) + } else { + fromJid = gateway.CHATNODE(update.ChatId) + } + gateway.SendTextMessage(c.jid, fromJid, text, c.xmpp, isGroupchat) } } @@ -443,7 +455,7 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { return } - gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) + gateway.SetNickname(c.jid, gateway.CHATNODE(update.ChatId), update.Title, c.xmpp) // set also the status (for group chats only) if user == nil { diff --git a/telegram/utils.go b/telegram/utils.go index ee3bc8b..c911001 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -459,14 +459,13 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o } c.locks.mucCacheLock.Lock() - chatJid := strconv.FormatInt(chatID, 10) + "@" + gateway.Jid.Full() + chatJid := gateway.CHATJID(chatID, true) for mucId, state := range c.mucCache { member, ok := state.Members[chatID] if ok { - sMucId := strconv.FormatInt(mucId, 10) newMucArgs := append( newArgs, - gateway.SPFrom(sMucId), + gateway.SPFrom(gateway.MUCNODE(mucId)), gateway.SPResource(member.Nickname), gateway.SPMUCAffiliation(member.Affiliation), gateway.SPMUCJid(chatJid), @@ -529,7 +528,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { c.mucCache[chatID] = mucState } - sChatId := strconv.FormatInt(chatID, 10) + sChatId := gateway.MUCNODE(chatID) myNickname := "me" if c.me != nil { myNickname = c.getFullName(c.me) @@ -542,8 +541,6 @@ func (c *Client) sendMUCStatuses(chatID int64) { Filter: &client.ChatMembersFilterMembers{}, }) if err == nil { - gatewayJidSuffix := "@" + gateway.Jid.Full() - for _, member := range members.Members { var senderId int64 switch member.MemberId.MessageSenderType() { @@ -573,7 +570,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(affiliation), - gateway.SPMUCJid(strconv.FormatInt(senderId, 10) + gatewayJidSuffix), + gateway.SPMUCJid(gateway.CHATJID(senderId, true)), ) } } @@ -592,7 +589,7 @@ func (c *Client) sendMUCSubject(chatID int64, resource string) { pin, err := c.client.GetChatPinnedMessage(&client.GetChatPinnedMessageRequest{ ChatId: chatID, }) - mucJid := strconv.FormatInt(chatID, 10) + "@" + gateway.Jid.Bare() + mucJid := gateway.MUCJID(chatID) toJid := c.jid + "/" + resource if err == nil { gateway.SendSubjectMessage( @@ -617,7 +614,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() - realJid := strconv.FormatInt(memberID, 10) + "@" + gateway.Jid.Full() + realJid := gateway.CHATJID(memberID, true) for mucId, state := range c.mucCache { oldMember, ok := state.Members[memberID] if ok { @@ -626,7 +623,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { Affiliation: oldMember.Affiliation, } - sMucId := strconv.FormatInt(mucId, 10) + sMucId := gateway.MUCNODE(mucId) unavailableStatusCodes := []uint16{303, 210} availableStatusCodes := []uint16{100, 210} if c.me != nil && memberID == c.me.Id { @@ -1445,7 +1442,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id senderId := c.getMessageSenderId(message) if senderId != 0 { - originalFrom = strconv.FormatInt(senderId, 10) + "@" + gateway.Jid.Full() + originalFrom = gateway.CHATJID(senderId, true) } } @@ -1552,7 +1549,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id var from string if groupChatFrom == "" { - from = strconv.FormatInt(chatId, 10) + from = gateway.CHATNODE(chatId) } else { from = groupChatFrom } @@ -1693,11 +1690,10 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) { - sChatId := strconv.FormatInt(chatID, 10) if isGroupchat { - gateway.SendErrorMessage(returnJid, sChatId + "@" + gateway.Jid.Bare(), text, code, isGroupchat, c.xmpp) + gateway.SendErrorMessage(returnJid, gateway.MUCJID(chatID), text, code, isGroupchat, c.xmpp) } else { - gateway.SendTextMessage(returnJid, sChatId, text, c.xmpp) + gateway.SendTextMessage(returnJid, gateway.CHATNODE(chatID), text, c.xmpp, isGroupchat) } } @@ -2005,7 +2001,7 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) { args = append(args, gateway.SPNickname(chat.Title)) - gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) + gateway.SetNickname(c.jid, gateway.CHATNODE(id), chat.Title, c.xmpp) } c.sendPresence(args...) @@ -2080,7 +2076,7 @@ func (c *Client) UpdateChatNicknames() { c.sendPresence(newArgs...) - gateway.SetNickname(c.jid, strconv.FormatInt(id, 10), chat.Title, c.xmpp) + gateway.SetNickname(c.jid, gateway.CHATNODE(id), chat.Title, c.xmpp) } } } @@ -2240,10 +2236,10 @@ func (c *Client) memberStatusToAffiliation(memberStatus client.ChatMemberStatus) } func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) { - sChatId := strconv.FormatInt(chatID, 10) + sChatId := gateway.CHATNODE(chatID) var mucJid string if toJid != "" { - mucJid = sChatId + "@" + gateway.Jid.Bare() + mucJid = gateway.MUCJID(chatID) } for i := len(messages) - 1; i >= 0; i-- { diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 990ba42..255fd76 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -61,6 +61,32 @@ var DirtySessions = false // MessageOutgoingPermissionVersion contains a XEP-0356 version to fake outgoing messages by foreign JIDs var MessageOutgoingPermissionVersion = 0 +// CHATNODE converts numeric id to node part of 1-1 chat JID +func CHATNODE(chatId int64) string { + return strconv.FormatInt(chatId, 10) +} + +// CHATJID converts numeric id to 1-1 chat JID +func CHATJID(chatId int64, full bool) string { + var suffix string + if full { + suffix = Jid.Full() + } else { + suffix = Jid.Bare() + } + return CHATNODE(chatId) + "@" + suffix +} + +// MUCNODE converts numeric id to node part of MUC JID +func MUCNODE(chatId int64) string { + return "c" + CHATNODE(chatId) +} + +// MUCJID converts numeric id to MUC JID +func MUCJID(chatId int64) string { + return "c" + CHATJID(chatId, false) +} + // SendMessage creates and sends a message stanza func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom string) { sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "") @@ -76,12 +102,12 @@ func SendServiceMessage(to, body string, component *xmpp.Component) { } // SendTextMessage creates and sends a simple message stanza -func SendTextMessage(to, from, body string, component *xmpp.Component) { +func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupchat bool) { var id string if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "") + sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "") } // SendErrorMessage creates and sends an error message stanza @@ -536,7 +562,7 @@ func SendPresence(component *xmpp.Component, to string, args ...args.V) error { // 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(oldArgs, SPFrom(CHATNODE(id))) newArgs = append(newArgs, SPResource(Jid.Resource)) return newArgs } @@ -626,7 +652,7 @@ func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId message := stanza.Message{ Attrs: stanza.Attrs{ - From: strconv.FormatInt(chatId, 10) + "@" + Jid.Bare(), + From: CHATJID(chatId, false), To: jid, Type: stanza.MessageTypeHeadline, }, @@ -637,6 +663,5 @@ func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId } func InviteToMUC(chatID int64, jid string, component *xmpp.Component) { - sChatID := strconv.FormatInt(chatID, 10) - SendMUCInvite(jid, sChatID, component, Jid.Full()) + SendMUCInvite(jid, MUCNODE(chatID), component, Jid.Full()) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 2f3cb0a..506d9e6 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -128,7 +128,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { return } - toID, ok := toToID(msg.To) + toID, ok, toIsGroup := toToID(msg.To) if ok { toJid, err := stanza.NewJid(msg.To) if err != nil { @@ -138,15 +138,22 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { isGroupchat := msg.Type == "groupchat" - if session.Session.MUC && toJid.Resource != "" { + if session.Session.MUC { chat, _, err := session.GetContactByID(toID, nil) if err == nil && session.IsGroup(chat) { - if isGroupchat { - gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "", msg.Id, 400, true, component) - } else { - gateway.SendErrorMessage(msg.From, msg.To, "PMing room members is not supported, use the real JID", 406, true, component) + if !toIsGroup { + gateway.SendErrorMessage(msg.From, toJid.Node, "KHVATIT SYUDA ZVONITb", 403, false, component) + return + } + + if toJid.Resource != "" { + if isGroupchat { + gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "", msg.Id, 400, true, component) + } else { + gateway.SendErrorMessage(msg.From, msg.To, "PMing room members is not supported, use the real JID", 406, true, component) + } + return } - return } } @@ -210,13 +217,21 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, replace.Id) if err == nil { if chatId != toID { - gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "", component) + if isGroupchat { + gateway.SendErrorMessage(msg.From, gateway.MUCJID(toID), text, 400, isGroupchat, component) + } else { + gateway.SendTextMessage(msg.From, gateway.CHATNODE(toID), "", component, isGroupchat) + } return } replaceId = msgId log.Debugf("replace tg: %#v %#v", chatId, msgId) } else { - gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "", component) + if isGroupchat { + gateway.SendErrorMessage(msg.From, gateway.MUCJID(toID), text, 400, isGroupchat, component) + } else { + gateway.SendTextMessage(msg.From, gateway.CHATNODE(toID), "", component, isGroupchat) + } return } } @@ -313,7 +328,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { if !ok { return } - toID, ok := toToID(msg.To) + toID, ok, _ := toToID(msg.To) if !ok { return } @@ -385,7 +400,7 @@ func handleSubscription(s xmpp.Sender, p stanza.Presence) { _ = gateway.ResumableSend(component, reply) - toID, ok := toToID(p.To) + toID, ok, _ := toToID(p.To) if !ok { return } @@ -502,8 +517,8 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen return } - chatId, ok := toToID(toBare) - if !ok { + chatId, ok, toIsGroup := toToID(toBare) + if !ok || !toIsGroup { presenceReplySetError(reply, 404) return } @@ -562,8 +577,8 @@ func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) { return } - chatId, ok := toToID(toBare) - if !ok { + chatId, ok, toIsGroup := toToID(toBare) + if !ok || !toIsGroup { return } @@ -663,7 +678,7 @@ func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGe return } - chatId, ok := toToID(iq.To) + chatId, ok, _ := toToID(iq.To) if !ok { log.Errorf("Invalid chat id in To JID %v", iq.To) return @@ -764,7 +779,7 @@ func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGe } func getTelegramChatType(from string, to string) (telegram.ChatType, error) { - toId, ok := toToID(to) + toId, ok, _ := toToID(to) if ok { bare, _, ok := gateway.SplitJID(from) if ok { @@ -809,14 +824,14 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { defer gateway.ResumableSend(component, answer) disco := answer.DiscoInfo() - toID, toOk := toToID(iq.To) + toID, toOk, toIsGroup := toToID(iq.To) if di.Node == "" { var isMuc bool bare, _, fromOk := gateway.SplitJID(iq.From) if fromOk { session, sessionOk := sessions[bare] - if sessionOk && session.Session.MUC { + if sessionOk && session.Session.MUC && toIsGroup { if toOk { chat, _, err := session.GetContactByID(toID, nil) if err == nil && session.IsGroup(chat) { @@ -907,7 +922,7 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { log.Debugf("discoItems: %#v", di) - toID, toOk := toToID(iq.To) + toID, toOk, _ := toToID(iq.To) disco := answer.DiscoItems() @@ -959,10 +974,9 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { // raw access, no need to create a new instance if not connected session, sessionOk := sessions[bare] if sessionOk && session.Session.MUC { - bareJid := gateway.Jid.Bare() - disco.AddItem(bareJid, "", "Telegram group chats") + disco.AddItem(gateway.Jid.Bare(), "", "Telegram group chats") for _, chat := range session.GetGroupChats() { - jid := strconv.FormatInt(chat.Id, 10) + "@" + bareJid + jid := gateway.MUCJID(chat.Id) disco.AddItem(jid, "", chat.Title) } } @@ -1085,7 +1099,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if !ok { return } - toId, toOk := toToID(iq.To) + toId, toOk, _ := toToID(iq.To) var cmdString string var cmdType telegram.CommandType @@ -1603,19 +1617,25 @@ func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram. } } -func toToID(to string) (int64, bool) { +func toToID(to string) (int64, bool, bool) { + var isGroup bool toParts := strings.Split(to, "@") if len(toParts) < 2 { - return 0, false + return 0, false, isGroup } - toID, err := strconv.ParseInt(toParts[0], 10, 64) + node := toParts[0] + if strings.HasPrefix(node, "c") { + isGroup = true + node = node[1:] + } + toID, err := strconv.ParseInt(node, 10, 64) if err != nil { log.WithFields(log.Fields{ "to": to, }).Error(errors.Wrap(err, "Invalid to JID!")) - return 0, false + return 0, false, isGroup } - return toID, true + return toID, true, isGroup } func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload { From f9f2bc8b83df433f0b3e32fe1faead6b7051f4fa Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 13 May 2025 23:52:55 -0400 Subject: [PATCH 120/228] Display incoming messages from Telegram in MUCs --- telegram/handlers.go | 30 +++++++++-- telegram/utils.go | 126 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 138 insertions(+), 18 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 021280d..e317ecb 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -309,7 +309,15 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } log.Infof("ignoredResource: %v", ignoredResource) - jids := c.getCarbonFullJids(true, ignoredResource) + chat, _, _ := c.GetContactByID(update.ChatId, nil) + isMUC := c.Session.MUC && c.IsGroup(chat) + + var jids []string + if isMUC { + _, jids = c.getMUCJoinedJIDs(update.ChatId) + } else { + c.getCarbonFullJids(true, ignoredResource) + } if len(jids) == 0 { log.Info("The only resource is ignored, aborting") return @@ -342,7 +350,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { }) var prefix string if messageErr == nil { - isCarbon = c.isCarbonsEnabled() && message.IsOutgoing + isCarbon = c.isCarbonsEnabled() && message.IsOutgoing && !isMUC // reply correction support in clients is suboptimal yet, so cut them out for now prefix, _ = c.messageToPrefix(message, "", "", true) } else { @@ -370,9 +378,23 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { markupFunction, )) - sChatId := strconv.FormatInt(update.ChatId, 10) + var from string + var originalFrom string + if isMUC { + var nickname string + if messageErr == nil { + senderId := c.getMessageSenderId(message) + nickname = c.GetMUCNickname(senderId) + originalFrom = gateway.CHATJID(senderId, true) + } else { + nickname = "#ERROR#" + } + from = gateway.MUCJID(update.ChatId) + "/" + nickname + } else { + from = gateway.CHATNODE(update.ChatId) + } for _, jid := range jids { - gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, 0, replaceId, isCarbon, false, false, "") + gateway.SendMessage(jid, from, text.String(), "e"+sId, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom) } } } diff --git a/telegram/utils.go b/telegram/utils.go index c911001..935beb7 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -585,6 +585,47 @@ func (c *Client) sendMUCStatuses(chatID int64) { ) } +func (c *Client) mucCacheHasMember(mucID int64, memberID int64) bool { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + mucState, ok := c.mucCache[mucID] + if !ok || mucState == nil { + return false // no MUC to be added to + } + + _, ok = mucState.Members[memberID] + return ok +} + +func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation string) bool { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + mucState, ok := c.mucCache[mucID] + if !ok || mucState == nil { + return false + } + + nickname := c.GetMUCNickname(memberID) + + err := c.sendPresence( + gateway.SPFrom(gateway.MUCNODE(mucID)), + gateway.SPResource(nickname), + gateway.SPImmed(true), + gateway.SPMUCAffiliation(affiliation), + gateway.SPMUCJid(gateway.CHATJID(memberID, true)), + ) + + if err == nil { + mucState.Members[memberID] = &MUCMember{ + Nickname: nickname, + Affiliation: affiliation, + } + return true + } + + return false +} + func (c *Client) sendMUCSubject(chatID int64, resource string) { pin, err := c.client.GetChatPinnedMessage(&client.GetChatPinnedMessageRequest{ ChatId: chatID, @@ -665,6 +706,24 @@ func (c *Client) MUCHasResource(chatID int64, resource string) bool { return ok } +func (c *Client) getMUCJoinedJIDs(chatId int64) (bool, []string) { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + groupChatTos := []string{} + + mucState, ok := c.mucCache[chatId] + if !ok || mucState == nil { + return false, nil + } else { + for resource := range mucState.Resources { + groupChatTos = append(groupChatTos, c.jid + "/" + resource) + } + } + + return true, groupChatTos +} + // GetMyMUCNickname obtains this account's nickname in a given MUC func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { if c.me == nil { @@ -1424,7 +1483,44 @@ func (c *Client) getPrefixSeparator(chatId int64) string { // ProcessIncomingMessage is a legacy wrapper for SendMessageToGateway aiming only PM messages func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { - c.SendMessageToGateway(chatId, message, "", false, "", []string{}) + chat, _, _ := c.GetContactByID(chatId, nil) + safeToSend := true + groupChatFrom := "" + groupChatTos := []string{} + if c.Session.MUC && c.IsGroup(chat) { + senderId := c.getMessageSenderId(message) + if senderId == 0 { + log.Errorf("Invalid sender id for message %#v", message) + return + } + + if !c.mucCacheHasMember(chatId, senderId) { + chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ + ChatId: chatId, + MemberId: message.SenderId, + }) + var status client.ChatMemberStatus + if err == nil { + status = chatMember.Status + } + safeToSend = c.addMUCMember(chatId, senderId, c.memberStatusToAffiliation(status)) + } + + groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId) + var ok bool + ok, groupChatTos = c.getMUCJoinedJIDs(chatId) + if !ok { + safeToSend = false + } + } + if safeToSend { + c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos) + } else { + mucJID := gateway.MUCJID(chatId) + for _, to := range groupChatTos { + gateway.SendErrorMessage(to, mucJID, "Cannot show a message", 500, true, c.xmpp) + } + } } // SendMessageToGateway transfers a message to XMPP side and marks it as read on Telegram side @@ -2218,19 +2314,21 @@ func (c *Client) usernamesToString(usernames []string) string { } func (c *Client) memberStatusToAffiliation(memberStatus client.ChatMemberStatus) string { - switch memberStatus.ChatMemberStatusType() { - case client.TypeChatMemberStatusCreator: - return "owner" - case client.TypeChatMemberStatusAdministrator: - return "admin" - case client.TypeChatMemberStatusMember: - return "member" - case client.TypeChatMemberStatusRestricted: - return "outcast" - case client.TypeChatMemberStatusLeft: - return "none" - case client.TypeChatMemberStatusBanned: - return "outcast" + if memberStatus != nil { + switch memberStatus.ChatMemberStatusType() { + case client.TypeChatMemberStatusCreator: + return "owner" + case client.TypeChatMemberStatusAdministrator: + return "admin" + case client.TypeChatMemberStatusMember: + return "member" + case client.TypeChatMemberStatusRestricted: + return "outcast" + case client.TypeChatMemberStatusLeft: + return "none" + case client.TypeChatMemberStatusBanned: + return "outcast" + } } return "member" } From 46ecab1db5919dc9d9ba70a33d7603229c2220fb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 14 May 2025 10:45:37 -0400 Subject: [PATCH 121/228] Fix regression in MUC list discovery --- xmpp/handlers.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 506d9e6..a82d315 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -831,8 +831,8 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { bare, _, fromOk := gateway.SplitJID(iq.From) if fromOk { session, sessionOk := sessions[bare] - if sessionOk && session.Session.MUC && toIsGroup { - if toOk { + if sessionOk && session.Session.MUC { + if toOk && toIsGroup { chat, _, err := session.GetContactByID(toID, nil) if err == nil && session.IsGroup(chat) { isMuc = true @@ -868,7 +868,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { disco.Form = stanza.NewForm(fields, "result") } - } else { + } else if !toOk { disco.AddFeatures( stanza.NSDiscoItems, "http://jabber.org/protocol/muc#stable_id", From dd00abe9778a2579de9154d6e8171831e9156fc0 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 14 May 2025 10:47:41 -0400 Subject: [PATCH 122/228] Process MUC registration requests --- telegram/utils.go | 7 +++++ xmpp/handlers.go | 73 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 935beb7..d32d69d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -648,6 +648,13 @@ func (c *Client) sendMUCSubject(chatID int64, resource string) { // GetMUCNickname generates a unique nickname for a MUC member func (c *Client) GetMUCNickname(chatID int64) string { + if chatID == 0 { + if c.me != nil { + chatID = c.me.Id + } else { + return "me" + } + } return c.FormatContact(chatID) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a82d315..7a499e7 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -847,6 +847,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { "muc_nonanonymous", "muc_unsecured", "http://jabber.org/protocol/muc#stable_id", + "jabber:iq:register", ) fields := []*stanza.Field{ &stanza.Field{ @@ -995,33 +996,56 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) { } defer gateway.ResumableSend(component, answer) - var login string + _, toOk, toIsGroup := toToID(iq.To) + bare, _, ok := gateway.SplitJID(iq.From) + var session *telegram.Client + var sessionOk bool if ok { - session, ok := sessions[bare] - if ok { - login = session.Session.Login - } + session, sessionOk = sessions[bare] } - var query stanza.IQPayload - if login == "" { - query = extensions.QueryRegister{ - Instructions: fmt.Sprintf("Authorization in Telegram is a multi-step process, so please accept %v to your contacts and follow further instructions (provide the authentication code there, etc.).\nFor now, please provide your login.", iq.To), + if toOk { + if toIsGroup { + nickname := "me" + if sessionOk { + nickname = session.GetMUCNickname(0) + } + answer.Payload = extensions.QueryRegister{ + Instructions: "MUC username is static", + Username: nickname, + Registered: &extensions.QueryRegisterRegistered{}, + } + } else { + query := extensions.QueryRegister{} + iqAnswerSetError(answer, &query, 404) + return } } else { - query = extensions.QueryRegister{ - Instructions: "Already logged in", - Username: login, - Registered: &extensions.QueryRegisterRegistered{}, + var login string + if sessionOk { + login = session.Session.Login } - } - answer.Payload = query - log.Debugf("%#v", query) + var query stanza.IQPayload + if login == "" { + query = extensions.QueryRegister{ + Instructions: fmt.Sprintf("Authorization in Telegram is a multi-step process, so please accept %v to your contacts and follow further instructions (provide the authentication code there, etc.).\nFor now, please provide your login.", iq.To), + } + } else { + query = extensions.QueryRegister{ + Instructions: "Already logged in", + Username: login, + Registered: &extensions.QueryRegisterRegistered{}, + } + } + answer.Payload = query - if login == "" { - gateway.SubscribeToTransport(component, iq.From) + log.Debugf("%#v", query) + + if login == "" { + gateway.SubscribeToTransport(component, iq.From) + } } } @@ -1032,6 +1056,12 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } defer gateway.ResumableSend(component, answer) + _, toOk, _ := toToID(iq.To) + if toOk { + iqAnswerSetError(answer, query, 400) + return + } + if query.Remove != nil { iqAnswerSetError(answer, query, 405) return @@ -1477,6 +1507,13 @@ func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code Type: stanza.ErrorTypeModify, Reason: "bad-request", } + case 404: + answer.Error = &stanza.Err{ + Code: code, + Type: stanza.ErrorTypeCancel, + Reason: "item-not-found", + Text: "No such room", + } case 405: answer.Error = &stanza.Err{ Code: code, From 538b9bca8bb5ce6c6e8cbbbbf09530f804d05019 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 17 May 2025 01:17:08 -0400 Subject: [PATCH 123/228] Add XEP-0359 IDs --- telegram/handlers.go | 8 ++++++- telegram/utils.go | 17 +++++++++----- xmpp/extensions/extensions.go | 25 +++++++++++++++++++++ xmpp/gateway/gateway.go | 42 ++++++++++++++++++++++++----------- xmpp/handlers.go | 6 ++++- 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index e317ecb..11c38ba 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -11,6 +11,7 @@ import ( "dev.narayana.im/narayana/telegabber/telegram/formatter" "dev.narayana.im/narayana/telegabber/xmpp/gateway" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/zelenin/go-tdlib/client" ) @@ -393,8 +394,13 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } else { from = gateway.CHATNODE(update.ChatId) } + id := "e"+sId + uuid, err := uuid.NewRandom() + if err == nil { + id = id+":"+uuid.String() + } for _, jid := range jids { - gateway.SendMessage(jid, from, text.String(), "e"+sId, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom) + gateway.SendMessage(jid, from, text.String(), id, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom, "") } } } diff --git a/telegram/utils.go b/telegram/utils.go index d32d69d..a902a76 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1644,10 +1644,14 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id // forward message to XMPP var sId string + var stanzaId string + strId := strconv.FormatInt(message.Id, 10) if id == "" { - sId = strconv.FormatInt(message.Id, 10) + sId = strId + stanzaId = strId } else { sId = id + stanzaId = strId } var from string @@ -1663,9 +1667,9 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id } for _, jid := range jids { - gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom) + gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) if auxText != "" { - gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom) + gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) } } c.UpdateLastChatMessageId(chatId, sId) @@ -2353,11 +2357,12 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p if plain { reply, _ := c.getMessageReply(message, false, true) + sId := strconv.FormatInt(message.Id, 10) gateway.SendMessage( c.jid, sChatId, c.formatMessage(0, 0, false, message), - strconv.FormatInt(message.Id, 10), + sId, c.xmpp, reply, 0, @@ -2366,12 +2371,14 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p false, false, "", + "", ) } else { + msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatID, message.Id) c.SendMessageToGateway( chatID, message, - "", + msgId, true, mucJid + "/" + c.GetMUCNickname(c.getMessageSenderId(message)), []string{toJid}, diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 2afc36c..2886e23 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -289,6 +289,19 @@ type MessageAddress struct { Jid string `xml:"jid,attr"` } +// MessageStanzaId is from XEP-0359 +type MessageStanzaId struct { + XMLName xml.Name `xml:"urn:xmpp:sid:0 stanza-id"` + Id string `xml:"id,attr"` + By string `xml:"by,attr"` +} + +// MessageOriginId is from XEP-0359 +type MessageOriginId struct { + XMLName xml.Name `xml:"urn:xmpp:sid:0 origin-id"` + Id string `xml:"id,attr"` +} + // EmptySubject is a dummy for MUCs to circumvent omitempty. Not registered as it would conflict with Subject field type EmptySubject struct { XMLName xml.Name `xml:"subject"` @@ -488,4 +501,16 @@ func init() { "http://jabber.org/protocol/address", "addresses", }, MessageAddresses{}) + + // stable stanza id + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:sid:0", + "stanza-id", + }, MessageStanzaId{}) + + // message addresses + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:sid:0", + "origin-id", + }, MessageOriginId{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 255fd76..2bddf83 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -88,8 +88,8 @@ func MUCJID(chatId int64) string { } // SendMessage creates and sends a message stanza -func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "") +func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId) } // SendServiceMessage creates and sends a simple message stanza from transport @@ -98,7 +98,7 @@ func SendServiceMessage(to, body string, component *xmpp.Component) { if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "") + sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "") } // SendTextMessage creates and sends a simple message stanza @@ -107,27 +107,27 @@ func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupch if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "") + sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "") } // SendErrorMessage creates and sends an error message stanza func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "") + sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "") } // SendErrorMessageWithBody creates and sends an error message stanza with body payload func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "") + sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "") } // SendMessageWithOOB creates and sends a message stanza with OOB URL -func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "") +func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId) } // SendSubjectMessage creates and sends a MUC subject func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { - sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "") + sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "", "") } // SendMessageMarker creates and sends a message stanza with a XEP-0333 marker @@ -135,15 +135,15 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker sendMessageWrapper(to, from, "", "", "", "", component, nil, &marker{ Type: markerType, Id: markerId, - }, 0, "", "", false, false, false, false, "", 0, "") + }, 0, "", "", false, false, false, false, "", 0, "", "") } // SendMUCInvite creates and send a MUC invitation message func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) { - sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom) + sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "") } -func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom string) { +func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -158,19 +158,24 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component var logFrom string var messageFrom string var messageTo string + var bareFrom string if isGroupchat { logFrom = from messageFrom = from + bareFrom, _, _ = SplitJID(from) } else { if from == "" { logFrom = componentJid messageFrom = componentJid + bareFrom = componentJid } else if inviteFrom != "" { logFrom = from messageFrom = from + "@" + Jid.Bare() + bareFrom = messageFrom } else { logFrom = from messageFrom = from + "@" + componentJid + bareFrom = from + "@" + Jid.Bare() } } if isCarbon { @@ -254,7 +259,7 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if timestamp != 0 { var delayFrom string if isGroupchat { - delayFrom, _, _ = SplitJID(from) + delayFrom = bareFrom } message.Extensions = append(message.Extensions, extensions.MessageDelay{ From: delayFrom, @@ -301,6 +306,17 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component Jid: messageFrom, }) } + if stanzaId != "" { + message.Extensions = append(message.Extensions, extensions.MessageStanzaId{ + Id: stanzaId, + By: bareFrom, + }) + if stanzaId != id { + message.Extensions = append(message.Extensions, extensions.MessageOriginId{ + Id: id, + }) + } + } if isCarbon { carbonMessage := extensions.ClientMessage{ diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 7a499e7..fa32087 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -181,7 +181,10 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } else { id := reply.Id if id[0] == 'e' { - id = id[1:] + idParts := strings.Split(id[1:], ":") + if len(idParts) >= 1 { + id = idParts[0] + } } replyId, err = strconv.ParseInt(id, 10, 64) if err != nil { @@ -848,6 +851,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { "muc_unsecured", "http://jabber.org/protocol/muc#stable_id", "jabber:iq:register", + "urn:xmpp:sid:0", ) fields := []*stanza.Field{ &stanza.Field{ From f1b57744906a297398eba20d5833dc4de7599664 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 18 May 2025 04:27:25 -0400 Subject: [PATCH 124/228] Take all history attributes into account conscientiously --- telegram/commands.go | 2 +- telegram/utils.go | 99 ++++++++++++++++++++++++++++++++++++++++---- xmpp/handlers.go | 22 ++++++++-- 3 files changed, 111 insertions(+), 12 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index ef73c37..f1554eb 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -1118,7 +1118,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } } - messages, err := c.getNLastMessages(chatID, limit) + messages, err := c.getNLastMessages(chatID, NewMessageLimitMessages(limit)) if err != nil { return err.Error(), true, false } diff --git a/telegram/utils.go b/telegram/utils.go index a902a76..ae66953 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -93,6 +93,44 @@ const ( MembersListBannedAndAdministrators ) +const ( + MessageLimitMessages = iota + MessageLimitChars + MessageLimitSince +) + +// MessageLimitType is an enum of MUC history limit types +type MessageLimitType int + +// MessageLimit stores a MUC history limit +type MessageLimit struct { + Type MessageLimitType + Messages int32 + Chars int + Since int64 +} + +func NewMessageLimitMessages(stanzas int32) *MessageLimit { + var limit MessageLimit + limit.Type = MessageLimitMessages + limit.Messages = stanzas + return &limit +} + +func NewMessageLimitChars(chars int) *MessageLimit { + var limit MessageLimit + limit.Type = MessageLimitChars + limit.Chars = chars + return &limit +} + +func NewMessageLimitSince(since int64) *MessageLimit { + var limit MessageLimit + limit.Type = MessageLimitSince + limit.Since = since + return &limit +} + // 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() { @@ -485,7 +523,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o } // JoinMUC saves MUC join fact and sends initialization data -func (c *Client) JoinMUC(chatId int64, resource string, limit int32) { +func (c *Client) JoinMUC(chatId int64, resource string, limit *MessageLimit) { // save the nickname in this MUC, also as a marker of join c.locks.mucCacheLock.Lock() mucState, ok := c.mucCache[chatId] @@ -1915,13 +1953,31 @@ func (c *Client) getLastMessages(id int64, query string, from int64, count int32 }) } -func (c *Client) getNLastMessages(chatID int64, limit int32) ([]*client.Message, error) { +func (c *Client) getNLastMessages(chatID int64, limit *MessageLimit) ([]*client.Message, error) { var newMessages *client.Messages var messages []*client.Message var err error var fromId int64 + var safetyLimit int32 + var charsCount int - for _ = range make([]struct{}, limit) { // safety limit + if limit == nil { + return nil, nil + } + switch limit.Type { + case MessageLimitMessages: + safetyLimit = limit.Messages + case MessageLimitChars: + safetyLimit = int32(limit.Chars) + if safetyLimit > 1000 { + safetyLimit = 1000 + } + case MessageLimitSince: + safetyLimit = 1000 + } + + safetyLoop: + for _ = range make([]struct{}, safetyLimit) { if len(messages) > 0 { fromId = messages[len(messages)-1].Id } @@ -1929,17 +1985,46 @@ func (c *Client) getNLastMessages(chatID int64, limit int32) ([]*client.Message, newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{ ChatId: chatID, FromMessageId: fromId, - Limit: limit, + Limit: safetyLimit, }) if err != nil { return nil, err } - messages = append(messages, newMessages.Messages...) - - if len(newMessages.Messages) == 0 || len(messages) >= int(limit) { + if len(newMessages.Messages) == 0 { break } + + for _, message := range newMessages.Messages { + if limit.Type == MessageLimitSince && limit.Since > int64(message.Date) { // durov… + break safetyLoop + } + + messages = append(messages, message) + + switch limit.Type { + case MessageLimitMessages: + if len(messages) >= int(limit.Messages) { + break safetyLoop + } + case MessageLimitChars: + // rough but why care + if message.Content != nil && message.Content.MessageContentType() == client.TypeMessageText { + textContent, ok := message.Content.(*client.MessageText) + if !ok { + uhOh() + } + + if textContent.Text != nil { + charsCount += len(textContent.Text.Text) + + if charsCount >= limit.Chars { + break safetyLoop + } + } + } + } + } } return messages, nil diff --git a/xmpp/handlers.go b/xmpp/handlers.go index fa32087..5868606 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -7,6 +7,7 @@ import ( "sort" "strconv" "strings" + "time" "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/telegram" @@ -544,11 +545,24 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen return } - limit, ok := mucExt.History.MaxStanzas.Get() - if !ok { - limit = 20 + log.Debugf("%#v", mucExt) + maxStanzas, maxStanzasOk := mucExt.History.MaxStanzas.Get() + maxChars, maxCharsOk := mucExt.History.MaxChars.Get() + seconds, secondsOk := mucExt.History.Seconds.Get() + + var limit *telegram.MessageLimit + if maxStanzasOk { + limit = telegram.NewMessageLimitMessages(int32(maxStanzas)) + } else if maxCharsOk { + limit = telegram.NewMessageLimitChars(maxChars) + } else if secondsOk { + limit = telegram.NewMessageLimitSince(time.Now().Add(time.Duration(seconds) * -time.Second).Unix()) + } else if !mucExt.History.Since.IsZero() { + limit = telegram.NewMessageLimitSince(mucExt.History.Since.Unix()) + } else { + limit = telegram.NewMessageLimitMessages(20) } - session.JoinMUC(chatId, fromResource, int32(limit)) + session.JoinMUC(chatId, fromResource, limit) } } } From 4d85e36bbf19a24913df4f7752ad114891e9e625 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 19 May 2025 13:46:34 -0400 Subject: [PATCH 125/228] Handle x-roomuser-item queries --- xmpp/handlers.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 5868606..1991d78 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -908,6 +908,20 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { disco.AddFeatures("jabber:iq:register") } disco.AddFeatures(gateway.NSCommand) + } else if di.Node == "x-roomuser-item" { + bare, _, fromOk := gateway.SplitJID(iq.From) + if fromOk { + session, sessionOk := sessions[bare] + if sessionOk && session.Session.MUC { + if toOk && toIsGroup { + chat, _, err := session.GetContactByID(toID, nil) + if err == nil && session.IsGroup(chat) { + disco.SetNode(di.Node) + disco.AddIdentity(session.GetMUCNickname(0), "conference", "text") + } + } + } + } } else { chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) From 29a76e342e5fe34ab73d4b9958c3526b578df990 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 23 May 2025 13:56:12 -0400 Subject: [PATCH 126/228] Handle MUC exits --- telegram/utils.go | 19 ++++++++++++++++++ xmpp/handlers.go | 51 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index ae66953..d10810d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -539,6 +539,8 @@ func (c *Client) JoinMUC(chatId int64, resource string, limit *MessageLimit) { } c.locks.mucCacheLock.Unlock() + log.Debugf("Resources in MUC %v: %v", chatId, mucState.Resources) + c.sendMUCStatuses(chatId) messages, err := c.getNLastMessages(chatId, limit) @@ -549,6 +551,23 @@ func (c *Client) JoinMUC(chatId int64, resource string, limit *MessageLimit) { c.sendMUCSubject(chatId, resource) } +// LeaveMUC removes MUC date from the cache +func (c *Client) LeaveMUC(chatId int64, resource string) { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + mucState, ok := c.mucCache[chatId] + if !ok || mucState == nil { + return + } + delete(mucState.Resources, resource) + log.Debugf("Resources in MUC %v: %v", chatId, mucState.Resources) + + if len(mucState.Resources) == 0 { + delete(c.mucCache, chatId) + } +} + func (c *Client) getFullName(user *client.User) string { fullName := user.FirstName if user.LastName != "" { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 1991d78..a914214 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -379,7 +379,7 @@ func HandlePresence(s xmpp.Sender, p stanza.Packet) { handleMUCPresence(s, prs, mucExt) return } - tryHandleMUCNicknameChange(s, prs) + tryHandleMUCPresence(s, prs) } func handleSubscription(s xmpp.Sender, p stanza.Presence) { @@ -567,11 +567,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen } } -func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) { - if p.Type != "" { - return - } - +func tryHandleMUCPresence(s xmpp.Sender, p stanza.Presence) { toBare, nickname, ok := gateway.SplitJID(p.To) if !ok || nickname == "" { return @@ -608,16 +604,25 @@ func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) { return } - log.Warn("🗿 Yes") - component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } + switch p.Type { + case "": + handleMUCNicknameChange(component, p, session, chatId, toBare) + case stanza.PresenceTypeUnavailable: + handleMUCUnavailable(component, p, session, chatId, fromResource) + } +} + +func handleMUCNicknameChange(component *xmpp.Component, p stanza.Presence, session *telegram.Client, chatId int64, toBare string) { + log.Warn("🗿 Yes") + from := toBare - nickname, ok = session.GetMyMUCNickname(chatId) + nickname, ok := session.GetMyMUCNickname(chatId) if ok { from = from+"/"+nickname } @@ -638,6 +643,34 @@ func tryHandleMUCNicknameChange(s xmpp.Sender, p stanza.Presence) { gateway.ResumableSend(component, reply) } +func handleMUCUnavailable(component *xmpp.Component, p stanza.Presence, session *telegram.Client, chatId int64, resource string) { + log.Warn("No, it's a MUC exit") + + session.LeaveMUC(chatId, resource) + + reply := &stanza.Presence{ + Attrs: stanza.Attrs{ + From: p.To, + To: p.From, + Id: p.Id, + Type: stanza.PresenceTypeUnavailable, + }, + Extensions: []stanza.PresExtension{ + extensions.PresenceXMucUserExtension{ + Item: extensions.PresenceXMucUserItem{ + Affiliation: "member", + Jid: p.From, + Role: "none", + }, + Statuses: []extensions.PresenceXMucUserStatus{ + extensions.PresenceXMucUserStatus{Code: 110}, + }, + }, + }, + } + gateway.ResumableSend(component, reply) +} + func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { log.WithFields(log.Fields{ "from": iq.From, From 2c8c7f029de3617d5e228f2265b9257d19537481 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 25 May 2025 22:35:26 -0400 Subject: [PATCH 127/228] Map MUC subject changes to new pinned messages in Telegram --- telegram/client.go | 3 +++ telegram/handlers.go | 14 ++++++++++++++ telegram/utils.go | 38 ++++++++++++++++++++++++++++++++++++++ xmpp/handlers.go | 28 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/telegram/client.go b/telegram/client.go index 56cd323..f10bf44 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -69,6 +69,7 @@ type Client struct { outbox map[string]string editOutbox map[string]string + pinOutbox map[int64]chan int64 DelayedStatuses map[int64]*DelayedStatus DelayedStatusesLock sync.Mutex @@ -98,6 +99,7 @@ type clientLocks struct { outboxLock sync.Mutex mucCacheLock sync.Mutex editOutboxLock sync.Mutex + pinOutboxLock sync.Mutex lastMsgHashesLock sync.Mutex lastMsgIdsLock sync.RWMutex @@ -167,6 +169,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component cache: cache.NewCache(), outbox: make(map[string]string), editOutbox: make(map[string]string), + pinOutbox: make(map[int64]chan int64), mucCache: make(map[int64]*MUCState), options: options, DelayedStatuses: make(map[int64]*DelayedStatus), diff --git a/telegram/handlers.go b/telegram/handlers.go index 11c38ba..a1fc001 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -452,6 +452,13 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat } func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { + c.locks.pinOutboxLock.Lock() + ch, chOk := c.pinOutbox[update.OldMessageId] + if chOk { + ch <-update.Message.Id + } + c.locks.pinOutboxLock.Unlock() + // 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 { @@ -469,6 +476,13 @@ func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucc } } func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) { + c.locks.pinOutboxLock.Lock() + ch, chOk := c.pinOutbox[update.OldMessageId] + if chOk { + ch <-0 + } + c.locks.pinOutboxLock.Unlock() + // clean uploaded files file, _ := c.contentToFile(update.Message.Content) if file != nil && file.Local != nil { diff --git a/telegram/utils.go b/telegram/utils.go index d10810d..1c78320 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -808,6 +808,44 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { return member.Nickname, true } +// NewPinnedMessage sends a text message and pins it right away +func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool { + c.locks.pinOutboxLock.Lock() + msg := c.ProcessOutgoingMessage(chatID, text, returnJid, 0, 0, true, true) + if msg == nil { + c.locks.pinOutboxLock.Unlock() + return false + } + ch := make(chan int64) + c.pinOutbox[msg.Id] = ch + c.locks.pinOutboxLock.Unlock() + + newId := <-ch + + c.locks.pinOutboxLock.Lock() + delete(c.pinOutbox, msg.Id) + c.locks.pinOutboxLock.Unlock() + + if newId == 0 { + return false + } + + ok, err := c.client.PinChatMessage(&client.PinChatMessageRequest{ + ChatId: chatID, + MessageId: newId, + }) + if err != nil { + log.Errorf("failed to pin message: %v", err.Error()) + c.client.DeleteMessages(&client.DeleteMessagesRequest{ + ChatId: chatID, + MessageIds: []int64{msg.Id}, + Revoke: true, + }) + } + + return ok != nil +} + // FormatContact retrieves a complete "full name (@usernames)" string for display func (c *Client) FormatContact(chatID int64) string { if chatID == 0 { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a914214..a1dee50 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -342,6 +342,34 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } return } + + if msg.Thread == "" && msg.Subject != "" && msg.Type == "groupchat" { + log.Debugf("MUC subject change: %#v", msg) + + bare, _, ok := gateway.SplitJID(msg.From) + if !ok { + return + } + session, ok := sessions[bare] + if !ok { + return + } + toID, ok, isGroup := toToID(msg.To) + if !ok || !isGroup { + return + } + _, resource, ok := gateway.SplitJID(msg.To) + if ok && resource != "" { + return + } + + go func() { + pinOk := session.NewPinnedMessage(toID, msg.Subject, msg.From) + if !pinOk { + gateway.SendErrorMessage(msg.From, gateway.MUCJID(toID), "", 406, true, component) + } + }() + } } if msg.Type == "error" { From dd8267df2cbf14b78a6c68e06a875735f36fc741 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 26 May 2025 21:58:35 -0400 Subject: [PATCH 128/228] Safeguards for MUC subject change --- telegram/client.go | 10 ++++++++-- telegram/handlers.go | 13 +++++++++++-- telegram/utils.go | 5 +++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index f10bf44..e1b84fe 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -47,6 +47,12 @@ type HashedAvatar struct { File int32 } +// IntPair holds two int64 values +type IntPair struct { + ChatId int64 + MessageId int64 +} + // Client stores the metadata for lazily invoked TDlib instance type Client struct { client *client.Client @@ -69,7 +75,7 @@ type Client struct { outbox map[string]string editOutbox map[string]string - pinOutbox map[int64]chan int64 + pinOutbox map[IntPair]chan int64 DelayedStatuses map[int64]*DelayedStatus DelayedStatusesLock sync.Mutex @@ -169,7 +175,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component cache: cache.NewCache(), outbox: make(map[string]string), editOutbox: make(map[string]string), - pinOutbox: make(map[int64]chan int64), + pinOutbox: make(map[IntPair]chan int64), mucCache: make(map[int64]*MUCState), options: options, DelayedStatuses: make(map[int64]*DelayedStatus), diff --git a/telegram/handlers.go b/telegram/handlers.go index a1fc001..d18d356 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -407,6 +407,15 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { // message(s) deleted func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { + c.locks.pinOutboxLock.Lock() + for _, messageId := range update.MessageIds { + ch, chOk := c.pinOutbox[IntPair{update.ChatId, messageId}] + if chOk { + ch <-0 + } + } + c.locks.pinOutboxLock.Unlock() + if update.IsPermanent { if c.Session.IsChatIgnored(update.ChatId) { return @@ -453,7 +462,7 @@ func (c *Client) updateAuthorizationState(update *client.UpdateAuthorizationStat func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucceeded) { c.locks.pinOutboxLock.Lock() - ch, chOk := c.pinOutbox[update.OldMessageId] + ch, chOk := c.pinOutbox[IntPair{update.Message.ChatId, update.OldMessageId}] if chOk { ch <-update.Message.Id } @@ -477,7 +486,7 @@ func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucc } func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) { c.locks.pinOutboxLock.Lock() - ch, chOk := c.pinOutbox[update.OldMessageId] + ch, chOk := c.pinOutbox[IntPair{update.Message.ChatId, update.OldMessageId}] if chOk { ch <-0 } diff --git a/telegram/utils.go b/telegram/utils.go index 1c78320..29d08d8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -817,13 +817,14 @@ func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool { return false } ch := make(chan int64) - c.pinOutbox[msg.Id] = ch + key := IntPair{chatID, msg.Id} + c.pinOutbox[key] = ch c.locks.pinOutboxLock.Unlock() newId := <-ch c.locks.pinOutboxLock.Lock() - delete(c.pinOutbox, msg.Id) + delete(c.pinOutbox, key) c.locks.pinOutboxLock.Unlock() if newId == 0 { From 1d165731a2ff37a7232557870b697887746d398a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 31 May 2025 00:10:07 -0400 Subject: [PATCH 129/228] Process MUC kicking --- telegram/commands.go | 6 +- telegram/utils.go | 60 ++++++++++++++++++++ xmpp/extensions/extensions.go | 31 ++++++++++ xmpp/handlers.go | 104 +++++++++++++++++++++++++++------- 4 files changed, 175 insertions(+), 26 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index f1554eb..f96598e 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -855,11 +855,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return "Contact not found", true, false } - _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ - ChatId: chatID, - MemberId: &client.MessageSenderUser{UserId: contact.Id}, - Status: &client.ChatMemberStatusLeft{}, - }) + err = c.Kick(chatID, contact.Id, "") if err != nil { return err.Error(), true, false } diff --git a/telegram/utils.go b/telegram/utils.go index 29d08d8..00b2380 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -808,6 +808,25 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { return member.Nickname, true } +// GetMUCMemberIdByNickname looks up the telegram ID by the MUC nickname (slow yet!) +func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + mucState, ok := c.mucCache[chatID] + if !ok || mucState == nil { + return 0 + } + + for memberId, member := range mucState.Members { + if member.Nickname == nickname { + return memberId + } + } + + return 0 +} + // NewPinnedMessage sends a text message and pins it right away func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool { c.locks.pinOutboxLock.Lock() @@ -1591,6 +1610,11 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { groupChatFrom := "" groupChatTos := []string{} if c.Session.MUC && c.IsGroup(chat) { + if message.Content.MessageContentType() == client.TypeMessageChatDeleteMember { + deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) + c.kickMemberFromMUC(chatId, deleteMember.UserId, c.GetMUCNickname(deleteMember.UserId)) + } + senderId := c.getMessageSenderId(message) if senderId == 0 { log.Errorf("Invalid sender id for message %#v", message) @@ -2623,3 +2647,39 @@ func (c *Client) MigrateToMUCs() { } } } + +// Kick kicks +func (c *Client) Kick(chatID, userID int64, nickname string) error { + _, err := c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ + ChatId: chatID, + MemberId: &client.MessageSenderUser{UserId: userID}, + Status: &client.ChatMemberStatusLeft{}, + }) + if err != nil && nickname != "" { + c.kickMemberFromMUC(chatID, userID, nickname) + } + return err +} + +func (c *Client) kickMemberFromMUC(chatID, userID int64, nickname string) { + unavailableStatusCodes := []uint16{307} + if c.me != nil && userID == c.me.Id { + unavailableStatusCodes = append(unavailableStatusCodes, 110) + } + c.sendPresence( + gateway.SPType("unavailable"), + gateway.SPFrom(gateway.MUCNODE(chatID)), + gateway.SPResource(nickname), + gateway.SPImmed(true), + gateway.SPMUCAffiliation("none"), + gateway.SPMUCStatusCodes(unavailableStatusCodes), + gateway.SPMUCJid(gateway.CHATJID(userID, true)), + ) + + c.locks.mucCacheLock.Lock() + mucState, ok := c.mucCache[chatID] + if ok && mucState != nil { + delete(mucState.Members, userID) + } + c.locks.mucCacheLock.Unlock() +} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 2886e23..64dfb3b 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -307,6 +307,21 @@ type EmptySubject struct { XMLName xml.Name `xml:"subject"` } +// QueryMucAdmin is from XEP-0045 +type QueryMucAdmin struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#admin query"` + Item QueryMucAdminItem `xml:"item"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + +// QueryMucAdminItem is a child element from XEP-0045 +type QueryMucAdminItem struct { + XMLName xml.Name `xml:"item"` + Nick string `xml:"nick,attr"` + Role string `xml:"role,attr"` + Reason string `xml:"reason,omitempty"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -392,6 +407,16 @@ func (ClientMessage) Name() string { return "message" } +// Namespace is a namespace! +func (c QueryMucAdmin) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c QueryMucAdmin) GetSet() *stanza.ResultSet { + return c.ResultSet +} + // NewReplyFallback initializes a fallback range func NewReplyFallback(start uint64, end uint64) Fallback { return Fallback{ @@ -513,4 +538,10 @@ func init() { "urn:xmpp:sid:0", "origin-id", }, MessageOriginId{}) + + // muc admin query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "http://jabber.org/protocol/muc#admin", + "query", + }, QueryMucAdmin{}) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a1dee50..d33c5d2 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -72,9 +72,9 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { return } } else if iq.Type == stanza.IQTypeSet { - query, ok := iq.Payload.(*extensions.QueryRegister) + queryRegister, ok := iq.Payload.(*extensions.QueryRegister) if ok { - go handleSetQueryRegister(s, iq, query) + go handleSetQueryRegister(s, iq, queryRegister) return } command, ok := iq.Payload.(*stanza.Command) @@ -82,6 +82,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleSetQueryCommand(s, iq, command) return } + queryMucAdmin, ok := iq.Payload.(*extensions.QueryMucAdmin) + if ok { + go handleSetQueryMucAdmin(s, iq, queryMucAdmin) + return + } } else if iq.Type == stanza.IQTypeResult { discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { @@ -1111,7 +1116,7 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) { } } else { query := extensions.QueryRegister{} - iqAnswerSetError(answer, &query, 404) + iqAnswerRegisterSetError(answer, &query, 404) return } } else { @@ -1151,12 +1156,12 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer _, toOk, _ := toToID(iq.To) if toOk { - iqAnswerSetError(answer, query, 400) + iqAnswerRegisterSetError(answer, query, 400) return } if query.Remove != nil { - iqAnswerSetError(answer, query, 405) + iqAnswerRegisterSetError(answer, query, 405) return } @@ -1174,7 +1179,7 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer if !ok { session, ok = getTelegramInstance(bare, &persistence.Session{}, component) if !ok { - iqAnswerSetError(answer, query, 500) + iqAnswerRegisterSetError(answer, query, 500) return } } @@ -1182,23 +1187,23 @@ func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer err := session.TryLogin(resource, query.Username) if err != nil { if err.Error() == telegram.TelegramAuthDone { - iqAnswerSetError(answer, query, 406) + iqAnswerRegisterSetError(answer, query, 406) } else { - iqAnswerSetError(answer, query, 500) + iqAnswerRegisterSetError(answer, query, 500) } return } err = session.SetPhoneNumber(query.Username) if err != nil { - iqAnswerSetError(answer, query, 500) + iqAnswerRegisterSetError(answer, query, 500) return } // everything okay, the response should be empty with no payload/error at this point gateway.SubscribeToTransport(component, iq.From) } else { - iqAnswerSetError(answer, query, 406) + iqAnswerRegisterSetError(answer, query, 406) } } @@ -1590,51 +1595,108 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command log.Debugf("command response: %#v %v", answer.Payload, cancelSend) } -func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { - answer.Type = stanza.IQTypeError +func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucAdmin) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + defer gateway.ResumableSend(component, answer) + + if query.Item.Role == "none" { + bare, _, fromOk := gateway.SplitJID(iq.From) + if !fromOk { + iqAnswerSetError(answer, 400) + return + } + + session, sessionOk := sessions[bare] + if !sessionOk || !session.Session.MUC { + iqAnswerSetError(answer, 401) + return + } + + toID, toOk, toIsGroup := toToID(iq.To) + if !toOk || !toIsGroup { + iqAnswerSetError(answer, 406) + return + } + + userID := session.GetMUCMemberIdByNickname(toID, query.Item.Nick) + if userID == 0 { + iqAnswerSetError(answer, 404) + return + } + + err := session.Kick(toID, userID, query.Item.Nick) + if err != nil { + iqAnswerSetError(answer, 500) + answer.Error.Text = err.Error() + return + } + } +} + +func iqAnswerSetError(answer *stanza.IQ, code int) { + iqAnswerSetErrorInternal(answer, code, false) +} + +func iqAnswerRegisterSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { answer.Payload = *payload + iqAnswerSetErrorInternal(answer, code, true) +} + +func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) { + answer.Type = stanza.IQTypeError switch code { case 400: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeModify, Reason: "bad-request", } + case 401: + answer.Error = &stanza.Err{ + Type: stanza.ErrorTypeAuth, + Reason: "not-authorized", + } case 404: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeCancel, Reason: "item-not-found", - Text: "No such room", } case 405: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeCancel, Reason: "not-allowed", - Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport", } case 406: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeModify, Reason: "not-acceptable", - Text: "Phone number already provided, chat with the transport for further instruction", } case 500: answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeWait, Reason: "internal-server-error", } default: log.Error("Unknown error code, falling back with empty reason") answer.Error = &stanza.Err{ - Code: code, Type: stanza.ErrorTypeCancel, Reason: "undefined-condition", } } + answer.Error.Code = code + + if registerMode { + switch code { + case 404: + answer.Error.Text = "No such room" + case 405: + answer.Error.Text = "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport" + case 406: + answer.Error.Text = "Phone number already provided, chat with the transport for further instruction" + } + } } func presenceReplySetError(reply *stanza.Presence, code int) { From 5f4165ac13612848aedde2c76995ecd9abd2af76 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 2 Jun 2025 10:56:26 -0400 Subject: [PATCH 130/228] Ignore prefix for OOB in channels too --- Makefile | 2 +- telegabber.go | 2 +- telegram/utils.go | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 80607e2..bfac3a1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.1" +VERSION := "v1.12.2" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 7d9a329..f4bed43 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.1" +var version string = "1.12.2" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index 789f635..592cde1 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1251,10 +1251,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { var ignorePrefix bool if oobSwap { if text == "" || message.Content.MessageContentType() == client.TypeMessageSticker { - isPM, err := c.IsPM(chatId) - if err == nil { - ignorePrefix = isPM && c.isCarbonsEnabled() - } + chatType, err := c.GetChatType(chatId) + ignorePrefix = err == nil && (chatType != ChatTypeBasicGroup && chatType != ChatTypeSupergroup) && c.isCarbonsEnabled() } } From 06964d832e58b30a82438072d174d3d30610463a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 7 Jun 2025 03:24:48 -0400 Subject: [PATCH 131/228] Get rid of edited message hash comparison in favour of EditDate check --- Makefile | 2 +- telegabber.go | 2 +- telegram/client.go | 3 --- telegram/handlers.go | 38 ++++++++++++++--------------- telegram/utils.go | 57 -------------------------------------------- 5 files changed, 20 insertions(+), 82 deletions(-) diff --git a/Makefile b/Makefile index bfac3a1..0022e89 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.2" +VERSION := "v1.12.3" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index f4bed43..3ef050c 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.2" +var version string = "1.12.3" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/client.go b/telegram/client.go index daaf627..005ec2f 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -2,7 +2,6 @@ package telegram import ( "github.com/pkg/errors" - "hash/maphash" "path/filepath" "strconv" "sync" @@ -56,7 +55,6 @@ type Client struct { lastMsgHashes map[int64]uint64 lastMsgIds map[int64]string - msgHashSeed maphash.Seed LastBotCmdString string @@ -149,7 +147,6 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component DelayedStatuses: make(map[int64]*DelayedStatus), lastMsgHashes: make(map[int64]uint64), lastMsgIds: make(map[int64]string), - msgHashSeed: maphash.MakeSeed(), XmppClientFeatures: make(map[string]*[]string), AvatarHashes: make(map[int64]*HashedAvatar), locks: clientLocks{ diff --git a/telegram/handlers.go b/telegram/handlers.go index d1e4dce..5a193f6 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -245,8 +245,6 @@ func (c *Client) updateNewMessage(update *client.UpdateNewMessage) { lock.Lock() defer lock.Unlock() - 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 { @@ -283,8 +281,6 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { markupFunction := c.getFormatter() - defer c.updateLastMessageHash(update.ChatId, update.MessageId, update.NewContent) - log.Debugf("newContent: %#v", update.NewContent) lock := c.getChatMessageLock(update.ChatId) @@ -308,7 +304,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { return } - if update.NewContent.MessageContentType() == client.TypeMessageText && c.hasLastMessageHashChanged(update.ChatId, update.MessageId, update.NewContent) { + if update.NewContent.MessageContentType() == client.TypeMessageText { textContent := update.NewContent.(*client.MessageText) log.Debugf("textContent: %#v", textContent.Text) @@ -316,6 +312,23 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { sId := strconv.FormatInt(update.MessageId, 10) var isCarbon bool + message, messageErr := c.client.GetMessage(&client.GetMessageRequest{ + ChatId: update.ChatId, + MessageId: update.MessageId, + }) + var prefix string + if messageErr == nil { + if message.EditDate == 0 { + return + } + + 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 is a carbon and if it is edited", update.ChatId, update.MessageId) + } + // 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) @@ -329,19 +342,6 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } } - message, messageErr := c.client.GetMessage(&client.GetMessageRequest{ - ChatId: update.ChatId, - MessageId: update.MessageId, - }) - 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) - } - var text strings.Builder if replaceId == "" { @@ -411,8 +411,6 @@ 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 diff --git a/telegram/utils.go b/telegram/utils.go index 592cde1..8e98326 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -4,10 +4,8 @@ import ( "bytes" "crypto/sha1" "encoding/base64" - "encoding/binary" "fmt" "github.com/pkg/errors" - "hash/maphash" "io" "io/ioutil" "net/http" @@ -1758,61 +1756,6 @@ func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []st return jids } -func (c *Client) calculateMessageHash(messageId int64, content client.MessageContent) uint64 { - var h maphash.Hash - h.SetSeed(c.msgHashSeed) - - buf8 := make([]byte, 8) - binary.BigEndian.PutUint64(buf8, uint64(messageId)) - h.Write(buf8) - - if content != nil && content.MessageContentType() == client.TypeMessageText { - textContent, ok := content.(*client.MessageText) - if !ok { - uhOh() - } - - if textContent.Text != nil { - h.WriteString(textContent.Text.Text) - for _, entity := range textContent.Text.Entities { - buf4 := make([]byte, 4) - binary.BigEndian.PutUint32(buf4, uint32(entity.Offset)) - h.Write(buf4) - binary.BigEndian.PutUint32(buf4, uint32(entity.Length)) - h.Write(buf4) - h.WriteString(entity.Type.TextEntityTypeType()) - } - } - } - - return h.Sum64() -} - -func (c *Client) updateLastMessageHash(chatId, messageId int64, content client.MessageContent) { - c.locks.lastMsgHashesLock.Lock() - defer c.locks.lastMsgHashesLock.Unlock() - - c.lastMsgHashes[chatId] = c.calculateMessageHash(messageId, content) -} - -func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content client.MessageContent) bool { - c.locks.lastMsgHashesLock.Lock() - defer c.locks.lastMsgHashesLock.Unlock() - - oldHash, ok := c.lastMsgHashes[chatId] - newHash := c.calculateMessageHash(messageId, content) - - if !ok { - log.Warnf("Last message hash for chat %v does not exist", chatId) - } - log.WithFields(log.Fields{ - "old hash": oldHash, - "new hash": newHash, - }).Info("Message hashes") - - return !ok || oldHash != newHash -} - func (c *Client) UpdateLastChatMessageId(chatId int64, messageId string) { c.locks.lastMsgIdsLock.Lock() defer c.locks.lastMsgIdsLock.Unlock() From 3fd49923a1e38ad9aa2ec8e11ec17205beca8552 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 7 Jun 2025 18:02:46 -0400 Subject: [PATCH 132/228] Safety limit for avatars --- Makefile | 2 +- telegabber.go | 2 +- telegram/utils.go | 23 ++++++++++++++++------- xmpp/handlers.go | 3 ++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 0022e89..dca71a3 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.3" +VERSION := "v1.12.4" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index 3ef050c..c68ade6 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.3" +var version string = "1.12.4" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/utils.go b/telegram/utils.go index 8e98326..3cf2a05 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -91,6 +91,8 @@ const ( MembersListBannedAndAdministrators ) +const AVATAR_SIZE_LIMIT int64 = 128 * 1024 + // 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() { @@ -315,6 +317,12 @@ func (c *Client) getFileData(tgFile *client.File, typ byte) string { priority = 32 } + // avoid not-well-formed stanza errors + if typ == typeFileDataBase64 && c.GetPhotoSize(tgFile) > AVATAR_SIZE_LIMIT { + log.Warnf("Photo %v skipped as it's too huge", tgFile.Id) + return "" + } + file, path, err := c.ForceOpenFile(tgFile, priority) if err == nil { defer file.Close() @@ -357,16 +365,16 @@ func (c *Client) SetEmptyAvatarHash(chatId int64) { c.AvatarHashesLock.Unlock() } -// GetPhotoSha1AndSize obtains data for PEP -func (c *Client) GetPhotoSha1AndSize(photo *client.File, chatId int64) (string, int64) { - sha1 := c.GetPhotoSha1(photo, chatId) - +// GetPhotoSize return at least a rough size +func (c *Client) GetPhotoSize(photo *client.File) int64 { + if photo == nil { + return 0 + } size := photo.Size if size == 0 { size = photo.ExpectedSize } - - return sha1, size + return size } // GetPhotoSha1 computes the photo hash @@ -1214,7 +1222,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { if chat.Photo == nil { c.SetEmptyAvatarHash(chatId) } else { - sha1, size := c.GetPhotoSha1AndSize(chat.Photo.Small, chatId) + sha1 := c.GetPhotoSha1(chat.Photo.Small, chatId) + size := c.GetPhotoSize(chat.Photo.Small) for resource := range c.resourcesRange() { features, ok := c.XmppClientFeatures[resource] diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c5560ec..3c3b370 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1375,7 +1375,8 @@ func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram. continue } - sha1, size := session.GetPhotoSha1AndSize(chat.Photo.Small, chat.Id) + sha1 := session.GetPhotoSha1(chat.Photo.Small, chat.Id) + size := session.GetPhotoSize(chat.Photo.Small) gateway.SendPubSubAvatarNotification(component, jid, chat.Id, sha1, size) } From 26eafbba3d08f8836fa9ccf40eda2e540f651239 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 14 Jun 2025 11:21:39 -0400 Subject: [PATCH 133/228] Merge fix --- telegram/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index c667d53..dd919ee 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1728,7 +1728,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id var ignorePrefix bool if oobSwap { if text == "" || message.Content.MessageContentType() == client.TypeMessageSticker { - chatType, err := c.GetChatType(chatId) + chatType, _, err := c.GetChatType(chatId) ignorePrefix = err == nil && (chatType != ChatTypeBasicGroup && chatType != ChatTypeSupergroup) && c.isCarbonsEnabled() } } From ebdf180ce5dee2575807c7fcae657d75a50df59b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 14 Jun 2025 13:20:02 -0400 Subject: [PATCH 134/228] Reflect MUC member affiliation/role changes in Telegram --- telegram/client.go | 1 + telegram/commands.go | 52 ++---------- telegram/utils.go | 156 ++++++++++++++++++++++++++++------ xmpp/extensions/extensions.go | 10 ++- xmpp/gateway/gateway.go | 13 ++- xmpp/handlers.go | 104 +++++++++++++++++------ 6 files changed, 234 insertions(+), 102 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index df983eb..c6acde1 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -31,6 +31,7 @@ type MUCState struct { type MUCMember struct { Nickname string Affiliation string + Role string } func NewMUCState() *MUCState { diff --git a/telegram/commands.go b/telegram/commands.go index f96598e..b43f2f8 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -855,7 +855,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return "Contact not found", true, false } - err = c.Kick(chatID, contact.Id, "") + err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusKicked, 0, "", "") if err != nil { return err.Error(), true, false } @@ -878,15 +878,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } } - _, 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, - }, - }) + err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusMuted, hours, "", "") if err != nil { return err.Error(), true, false } @@ -907,15 +899,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return "Contact not found", true, false } - _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ - ChatId: chatID, - MemberId: &client.MessageSenderUser{UserId: contact.Id}, - Status: &client.ChatMemberStatusRestricted{ - IsMember: true, - RestrictedUntilDate: 0, - Permissions: &permissionsMember, - }, - }) + err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusUnmuted, 0, "", "") if err != nil { return err.Error(), true, false } @@ -943,13 +927,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } } - _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ - ChatId: chatID, - MemberId: &client.MessageSenderUser{UserId: contact.Id}, - Status: &client.ChatMemberStatusBanned{ - BannedUntilDate: c.formatBantime(hours), - }, - }) + err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusBanned, hours, "", "") if err != nil { return err.Error(), true, false } @@ -963,11 +941,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return "Contact not found", true, false } - _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ - ChatId: chatID, - MemberId: &client.MessageSenderUser{UserId: contact.Id}, - Status: &client.ChatMemberStatusMember{}, - }) + err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusUnbanned, 0, "", "") if err != nil { return err.Error(), true, false } @@ -981,21 +955,13 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return "Contact not found", true, false } - // clone the permissions - status := client.ChatMemberStatusAdministrator{ - CanBeEdited: true, - Rights: &permissionsAdmin, - } - + var customTitle string if len(args) > 1 { - status.CustomTitle = args[1] + customTitle = args[1] } - _, err = c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ - ChatId: chatID, - MemberId: &client.MessageSenderUser{UserId: contact.Id}, - Status: &status, - }) + // clone the permissions + err = c.SetChatMemberStatus(chatID, contact.Id, ChatMemberStatusPromoted, 0, customTitle, "") if err != nil { return err.Error(), true, false } diff --git a/telegram/utils.go b/telegram/utils.go index dd919ee..f437fc9 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -131,6 +131,17 @@ func NewMessageLimitSince(since int64) *MessageLimit { const AVATAR_SIZE_LIMIT int64 = 128 * 1024 +const ( + ChatMemberStatusIllegal = iota + ChatMemberStatusKicked + ChatMemberStatusMuted + ChatMemberStatusUnmuted + ChatMemberStatusBanned + ChatMemberStatusUnbanned + ChatMemberStatusPromoted +) +type ChatMemberStatus int + // 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() { @@ -512,6 +523,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o gateway.SPFrom(gateway.MUCNODE(mucId)), gateway.SPResource(member.Nickname), gateway.SPMUCAffiliation(member.Affiliation), + gateway.SPMUCRole(member.Role), gateway.SPMUCJid(chatJid), ) err := c.sendPresence(newMucArgs...) @@ -597,6 +609,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { myNickname = c.getFullName(c.me) } myAffiliation := "member" + myRole := "participant" members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ ChatId: chatID, @@ -616,15 +629,17 @@ func (c *Client) sendMUCStatuses(chatID int64) { } nickname := c.GetMUCNickname(senderId) - affiliation := c.memberStatusToAffiliation(member.Status) + affiliation, role := c.memberStatusToAffiliationAndRole(member.Status) mucState.Members[senderId] = &MUCMember{ Nickname: nickname, Affiliation: affiliation, + Role: role, } if c.me != nil && senderId == c.me.Id { myNickname = nickname myAffiliation = affiliation + myRole = role continue } @@ -633,6 +648,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(affiliation), + gateway.SPMUCRole(affiliation), gateway.SPMUCJid(gateway.CHATJID(senderId, true)), ) } @@ -644,6 +660,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { gateway.SPResource(myNickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(myAffiliation), + gateway.SPMUCRole(myRole), gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), ) } @@ -660,7 +677,7 @@ func (c *Client) mucCacheHasMember(mucID int64, memberID int64) bool { return ok } -func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation string) bool { +func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation, role string) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[mucID] @@ -675,6 +692,7 @@ func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation string) b gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(affiliation), + gateway.SPMUCRole(role), gateway.SPMUCJid(gateway.CHATJID(memberID, true)), ) @@ -682,6 +700,7 @@ func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation string) b mucState.Members[memberID] = &MUCMember{ Nickname: nickname, Affiliation: affiliation, + Role: role, } return true } @@ -747,6 +766,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPResource(oldMember.Nickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(oldMember.Affiliation), + gateway.SPMUCRole(oldMember.Role), gateway.SPMUCNick(newNickname), gateway.SPMUCStatusCodes(unavailableStatusCodes), gateway.SPMUCJid(realJid), @@ -756,6 +776,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPResource(newNickname), gateway.SPImmed(true), gateway.SPMUCAffiliation(oldMember.Affiliation), + gateway.SPMUCRole(oldMember.Role), gateway.SPMUCStatusCodes(availableStatusCodes), gateway.SPMUCJid(realJid), ) @@ -1618,7 +1639,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { if c.Session.MUC && c.IsGroup(chat) { if message.Content.MessageContentType() == client.TypeMessageChatDeleteMember { deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) - c.kickMemberFromMUC(chatId, deleteMember.UserId, c.GetMUCNickname(deleteMember.UserId)) + c.mucMemberRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) } senderId := c.getMessageSenderId(message) @@ -1636,7 +1657,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { if err == nil { status = chatMember.Status } - safeToSend = c.addMUCMember(chatId, senderId, c.memberStatusToAffiliation(status)) + affiliation, role := c.memberStatusToAffiliationAndRole(status) + safeToSend = c.addMUCMember(chatId, senderId, affiliation, role) } groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId) @@ -2441,24 +2463,28 @@ func (c *Client) usernamesToString(usernames []string) string { return strings.Join(atUsernames, ", ") } -func (c *Client) memberStatusToAffiliation(memberStatus client.ChatMemberStatus) string { +func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMemberStatus) (string, string) { if memberStatus != nil { switch memberStatus.ChatMemberStatusType() { case client.TypeChatMemberStatusCreator: - return "owner" + return "owner", "moderator" case client.TypeChatMemberStatusAdministrator: - return "admin" + return "admin", "moderator" case client.TypeChatMemberStatusMember: - return "member" + return "member", "participant" case client.TypeChatMemberStatusRestricted: - return "outcast" + restricted, _ := memberStatus.(*client.ChatMemberStatusRestricted) + if restricted.Permissions != nil && !restricted.Permissions.CanSendBasicMessages { + return "member", "visitor" + } + return "member", "participant" case client.TypeChatMemberStatusLeft: - return "none" + return "none", "none" case client.TypeChatMemberStatusBanned: - return "outcast" + return "outcast", "none" } } - return "member" + return "member", "participant" } func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) { @@ -2598,38 +2624,114 @@ func (c *Client) MigrateToMUCs() { } } -// Kick kicks -func (c *Client) Kick(chatID, userID int64, nickname string) error { +// SetChatMemberStatus is a handy wrapper for the following TDLib method +func (c *Client) SetChatMemberStatus(chatID, userID int64, status ChatMemberStatus, numericPayload int64, stringPayload, nickname string) error { + var chatMemberStatus client.ChatMemberStatus + switch status { + case ChatMemberStatusKicked: + chatMemberStatus = &client.ChatMemberStatusLeft{} + case ChatMemberStatusMuted: + chatMemberStatus = &client.ChatMemberStatusRestricted{ + IsMember: true, + RestrictedUntilDate: c.formatBantime(numericPayload), + Permissions: &permissionsReadonly, + } + case ChatMemberStatusUnmuted: + chatMemberStatus = &client.ChatMemberStatusRestricted{ + IsMember: true, + RestrictedUntilDate: 0, + Permissions: &permissionsMember, + } + case ChatMemberStatusBanned: + chatMemberStatus = &client.ChatMemberStatusBanned{ + BannedUntilDate: c.formatBantime(numericPayload), + } + case ChatMemberStatusUnbanned: + chatMemberStatus = &client.ChatMemberStatusMember{} + case ChatMemberStatusPromoted: + chatMemberStatus = &client.ChatMemberStatusAdministrator{ + CanBeEdited: true, + Rights: &permissionsAdmin, + CustomTitle: stringPayload, + } + } _, err := c.client.SetChatMemberStatus(&client.SetChatMemberStatusRequest{ ChatId: chatID, MemberId: &client.MessageSenderUser{UserId: userID}, - Status: &client.ChatMemberStatusLeft{}, + Status: chatMemberStatus, }) - if err != nil && nickname != "" { - c.kickMemberFromMUC(chatID, userID, nickname) + if err == nil && nickname != "" { + c.mucMemberRolePresence(chatID, userID, status, nickname) } return err } -func (c *Client) kickMemberFromMUC(chatID, userID int64, nickname string) { - unavailableStatusCodes := []uint16{307} - if c.me != nil && userID == c.me.Id { - unavailableStatusCodes = append(unavailableStatusCodes, 110) - } - c.sendPresence( - gateway.SPType("unavailable"), +func (c *Client) mucMemberRolePresence(chatID, userID int64, status ChatMemberStatus, nickname string) { + args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), gateway.SPResource(nickname), gateway.SPImmed(true), - gateway.SPMUCAffiliation("none"), - gateway.SPMUCStatusCodes(unavailableStatusCodes), gateway.SPMUCJid(gateway.CHATJID(userID, true)), + } + var statusCodes []uint16 + var newAffiliation, newRole string + + switch status { + case ChatMemberStatusKicked: + args = append(args, gateway.SPType("unavailable")) + newAffiliation = "none" + newRole = "none" + + statusCodes = append(statusCodes, 307) + if c.me != nil && userID == c.me.Id { + statusCodes = append(statusCodes, 110) + } + case ChatMemberStatusMuted: + newAffiliation = "member" + newRole = "visitor" + case ChatMemberStatusUnmuted, ChatMemberStatusUnbanned: + newAffiliation = "member" + newRole = "participant" + case ChatMemberStatusBanned: + args = append(args, gateway.SPType("unavailable")) + newAffiliation = "outcast" + newRole = "none" + statusCodes = append(statusCodes, 301) + case ChatMemberStatusPromoted: + newAffiliation = "admin" + newRole = "moderator" + } + + args = append( + args, + gateway.SPMUCAffiliation(newAffiliation), + gateway.SPMUCRole(newRole), + gateway.SPMUCStatusCodes(statusCodes), ) + c.sendPresence(args...) + c.locks.mucCacheLock.Lock() mucState, ok := c.mucCache[chatID] if ok && mucState != nil { - delete(mucState.Members, userID) + if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned { + delete(mucState.Members, userID) + } else { + member, ok := mucState.Members[userID] + if ok { + member.Affiliation = newAffiliation + member.Role = newRole + } + } } c.locks.mucCacheLock.Unlock() } + +// GetErrorCode obtains an error code from a Telegram response error +func GetErrorCode(err error) (int32, bool) { + responseError, ok := err.(client.ResponseError) + if !ok || responseError.Err == nil { + return 0, false + } + return responseError.Err.Code, true +} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 64dfb3b..d8aafd7 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -316,10 +316,12 @@ type QueryMucAdmin struct { // QueryMucAdminItem is a child element from XEP-0045 type QueryMucAdminItem struct { - XMLName xml.Name `xml:"item"` - Nick string `xml:"nick,attr"` - Role string `xml:"role,attr"` - Reason string `xml:"reason,omitempty"` + XMLName xml.Name `xml:"item"` + Jid string `xml:"jid,attr,omitempty"` + Nick string `xml:"nick,attr,omitempty"` + Role string `xml:"role,attr,omitempty"` + Affiliation string `xml:"affiliation,attr,omitempty"` + Reason string `xml:"reason,omitempty"` } // Namespace is a namespace! diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 2bddf83..604a1eb 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -438,6 +438,9 @@ var SPImmed = args.NewBool(args.Default(true)) // SPMUCAffiliation is a XEP-0045 MUC affiliation var SPMUCAffiliation = args.NewString() +// SPMUCRole is a XEP-0045 MUC role +var SPMUCRole = args.NewString() + // SPMUCNick is a XEP-0045 MUC user nick var SPMUCNick = args.NewString() @@ -505,10 +508,16 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { if SPMUCAffiliation.IsSet(args) { affiliation := SPMUCAffiliation.Get(args) if affiliation != "" { + var role string + if SPMUCRole.IsSet(args) { + role = SPMUCRole.Get(args) + } else { + role = affiliationToRole(affiliation) + } userExt := extensions.PresenceXMucUserExtension{ Item: extensions.PresenceXMucUserItem{ Affiliation: affiliation, - Role: affilationToRole(affiliation), + Role: role, }, } if SPMUCNick.IsSet(args) { @@ -625,7 +634,7 @@ func SplitJID(from string) (string, string, bool) { return fromJid.Bare(), fromJid.Resource, true } -func affilationToRole(affilation string) string { +func affiliationToRole(affilation string) string { switch affilation { case "owner", "admin": return "moderator" diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c0cf696..76ed019 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1602,37 +1602,84 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } defer gateway.ResumableSend(component, answer) - if query.Item.Role == "none" { - bare, _, fromOk := gateway.SplitJID(iq.From) - if !fromOk { - iqAnswerSetError(answer, 400) - return - } + bare, _, fromOk := gateway.SplitJID(iq.From) + if !fromOk { + iqAnswerSetError(answer, 400) + return + } - session, sessionOk := sessions[bare] - if !sessionOk || !session.Session.MUC { - iqAnswerSetError(answer, 401) - return - } + session, sessionOk := sessions[bare] + if !sessionOk || !session.Session.MUC { + iqAnswerSetError(answer, 403) + return + } - toID, toOk, toIsGroup := toToID(iq.To) - if !toOk || !toIsGroup { - iqAnswerSetError(answer, 406) - return - } + toID, toOk, toIsGroup := toToID(iq.To) + if !toOk || !toIsGroup { + iqAnswerSetError(answer, 406) + return + } - userID := session.GetMUCMemberIdByNickname(toID, query.Item.Nick) - if userID == 0 { - iqAnswerSetError(answer, 404) - return - } + var userID int64 + if query.Item.Jid != "" { + userID, _, _ = toToID(query.Item.Jid) + } else if query.Item.Nick != "" { + userID = session.GetMUCMemberIdByNickname(toID, query.Item.Nick) + } + if userID == 0 { + iqAnswerSetError(answer, 404) + return + } - err := session.Kick(toID, userID, query.Item.Nick) - if err != nil { - iqAnswerSetError(answer, 500) - answer.Error.Text = err.Error() - return + nick := query.Item.Nick + if nick == "" { + nick = session.GetMUCNickname(userID) + } + + var status telegram.ChatMemberStatus + var integerPayload int64 + var stringPayload string + + switch query.Item.Role { + case "none": + status = telegram.ChatMemberStatusKicked + case "visitor": + status = telegram.ChatMemberStatusMuted + case "participant": + status = telegram.ChatMemberStatusUnmuted + case "moderator": + status = telegram.ChatMemberStatusPromoted + } + // affiliations have a higher priority over roles + switch query.Item.Affiliation { + case "none": + status = telegram.ChatMemberStatusKicked + case "outcast": + status = telegram.ChatMemberStatusBanned + case "member": + status = telegram.ChatMemberStatusUnmuted + case "admin": + status = telegram.ChatMemberStatusPromoted + case "owner": + iqAnswerSetError(answer, 403) + return + } + + // nothing has been detected + if status == telegram.ChatMemberStatusIllegal { + iqAnswerSetError(answer, 400) + return + } + + err := session.SetChatMemberStatus(toID, userID, status, integerPayload, stringPayload, nick) + if err != nil { + code, ok := telegram.GetErrorCode(err) + if !ok { + code = 500 } + iqAnswerSetError(answer, int(code)) + answer.Error.Text = err.Error() + return } } @@ -1658,6 +1705,11 @@ func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) { Type: stanza.ErrorTypeAuth, Reason: "not-authorized", } + case 403: + answer.Error = &stanza.Err{ + Type: stanza.ErrorTypeAuth, + Reason: "forbidden", + } case 404: answer.Error = &stanza.Err{ Type: stanza.ErrorTypeCancel, From 9ed3a8b8751168e5f9d693ef0aecb876190f792c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 14 Jun 2025 14:34:51 -0400 Subject: [PATCH 135/228] Why clog the code with uhOh, it panics anyway --- telegram/handlers.go | 69 +++++++++----------------------------------- telegram/utils.go | 5 +--- 2 files changed, 14 insertions(+), 60 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 96b4dfe..8d63971 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -16,10 +16,6 @@ import ( "github.com/zelenin/go-tdlib/client" ) -func uhOh() { - log.Fatal("Update type mismatch") -} - func int64SliceToStringSlice(ints []int64) []string { strings := make([]string, len(ints)) wg := sync.WaitGroup{} @@ -89,89 +85,50 @@ func (c *Client) updateHandler() { if update.GetClass() == client.ClassUpdate { switch update.GetType() { case client.TypeUpdateUser: - typedUpdate, ok := update.(*client.UpdateUser) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateUser) c.updateUser(typedUpdate) log.Debugf("%#v", typedUpdate.User) case client.TypeUpdateUserStatus: - typedUpdate, ok := update.(*client.UpdateUserStatus) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateUserStatus) c.updateUserStatus(typedUpdate) log.Debugf("%#v", typedUpdate.Status) case client.TypeUpdateNewChat: - typedUpdate, ok := update.(*client.UpdateNewChat) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateNewChat) c.updateNewChat(typedUpdate) log.Debugf("%#v", typedUpdate.Chat) case client.TypeUpdateChatPosition: - typedUpdate, ok := update.(*client.UpdateChatPosition) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateChatPosition) c.updateChatPosition(typedUpdate) log.Debugf("%#v", typedUpdate) case client.TypeUpdateChatLastMessage: - typedUpdate, ok := update.(*client.UpdateChatLastMessage) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateChatLastMessage) c.updateChatLastMessage(typedUpdate) log.Debugf("%#v", typedUpdate) case client.TypeUpdateNewMessage: - typedUpdate, ok := update.(*client.UpdateNewMessage) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateNewMessage) c.updateNewMessage(typedUpdate) log.Debugf("%#v", typedUpdate.Message) case client.TypeUpdateMessageContent: - typedUpdate, ok := update.(*client.UpdateMessageContent) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateMessageContent) c.updateMessageContent(typedUpdate) log.Debugf("%#v", typedUpdate.NewContent) case client.TypeUpdateDeleteMessages: - typedUpdate, ok := update.(*client.UpdateDeleteMessages) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateDeleteMessages) c.updateDeleteMessages(typedUpdate) case client.TypeUpdateAuthorizationState: - typedUpdate, ok := update.(*client.UpdateAuthorizationState) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateAuthorizationState) c.updateAuthorizationState(typedUpdate) case client.TypeUpdateMessageSendSucceeded: - typedUpdate, ok := update.(*client.UpdateMessageSendSucceeded) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateMessageSendSucceeded) c.updateMessageSendSucceeded(typedUpdate) case client.TypeUpdateMessageSendFailed: - typedUpdate, ok := update.(*client.UpdateMessageSendFailed) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateMessageSendFailed) c.updateMessageSendFailed(typedUpdate) case client.TypeUpdateChatTitle: - typedUpdate, ok := update.(*client.UpdateChatTitle) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateChatTitle) c.updateChatTitle(typedUpdate) case client.TypeUpdateChatReadOutbox: - typedUpdate, ok := update.(*client.UpdateChatReadOutbox) - if !ok { - uhOh() - } + typedUpdate, _ := update.(*client.UpdateChatReadOutbox) c.updateChatReadOutbox(typedUpdate) default: // log only handled types diff --git a/telegram/utils.go b/telegram/utils.go index f437fc9..65ee9d5 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2119,10 +2119,7 @@ func (c *Client) getNLastMessages(chatID int64, limit *MessageLimit) ([]*client. case MessageLimitChars: // rough but why care if message.Content != nil && message.Content.MessageContentType() == client.TypeMessageText { - textContent, ok := message.Content.(*client.MessageText) - if !ok { - uhOh() - } + textContent, _ := message.Content.(*client.MessageText) if textContent.Text != nil { charsCount += len(textContent.Text.Text) From 8ce5c8373f2869c391e66928b6edf1d9752eb829 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 15 Jun 2025 05:16:58 -0400 Subject: [PATCH 136/228] Process basic group member list updates --- telegram/handlers.go | 19 ++++++++++++ telegram/utils.go | 73 ++++++++++++++++++++------------------------ 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 8d63971..92e55c9 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -130,6 +130,9 @@ func (c *Client) updateHandler() { case client.TypeUpdateChatReadOutbox: typedUpdate, _ := update.(*client.UpdateChatReadOutbox) c.updateChatReadOutbox(typedUpdate) + case client.TypeUpdateBasicGroupFullInfo: + typedUpdate, _ := update.(*client.UpdateBasicGroupFullInfo) + c.updateBasicGroupFullInfo(typedUpdate) default: // log only handled types continue @@ -477,3 +480,19 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { func (c *Client) updateChatReadOutbox(update *client.UpdateChatReadOutbox) { c.sendMarker(update.ChatId, update.LastReadOutboxMessageId, gateway.MarkerTypeDisplayed) } + +func (c *Client) updateBasicGroupFullInfo(update *client.UpdateBasicGroupFullInfo) { + if c.Session.MUC && update.BasicGroupFullInfo != nil { + chatID := -update.BasicGroupId + + c.locks.mucCacheLock.Lock() + + mucState, ok := c.mucCache[chatID] + if ok && mucState != nil { + mucState.Members = make(map[int64]*MUCMember) + c.updateMUCMembers(mucState, chatID, update.BasicGroupFullInfo.Members) + } + + c.locks.mucCacheLock.Unlock() + } +} diff --git a/telegram/utils.go b/telegram/utils.go index 65ee9d5..b166551 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -603,6 +603,15 @@ func (c *Client) sendMUCStatuses(chatID int64) { c.mucCache[chatID] = mucState } + members, _ := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ + ChatId: chatID, + Limit: 200, + Filter: &client.ChatMembersFilterMembers{}, + }) + c.updateMUCMembers(mucState, chatID, members.Members) +} + +func (c *Client) updateMUCMembers(mucState *MUCState, chatID int64, members []*client.ChatMember) { sChatId := gateway.MUCNODE(chatID) myNickname := "me" if c.me != nil { @@ -611,47 +620,31 @@ func (c *Client) sendMUCStatuses(chatID int64) { myAffiliation := "member" myRole := "participant" - members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ - ChatId: chatID, - Limit: 200, - Filter: &client.ChatMembersFilterMembers{}, - }) - if err == nil { - 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 - } - - nickname := c.GetMUCNickname(senderId) - affiliation, role := c.memberStatusToAffiliationAndRole(member.Status) - mucState.Members[senderId] = &MUCMember{ - Nickname: nickname, - Affiliation: affiliation, - Role: role, - } - - if c.me != nil && senderId == c.me.Id { - myNickname = nickname - myAffiliation = affiliation - myRole = role - continue - } - - c.sendPresence( - gateway.SPFrom(sChatId), - gateway.SPResource(nickname), - gateway.SPImmed(true), - gateway.SPMUCAffiliation(affiliation), - gateway.SPMUCRole(affiliation), - gateway.SPMUCJid(gateway.CHATJID(senderId, true)), - ) + for _, member := range members { + senderId := c.GetSenderId(member.MemberId) + nickname := c.GetMUCNickname(senderId) + affiliation, role := c.memberStatusToAffiliationAndRole(member.Status) + mucState.Members[senderId] = &MUCMember{ + Nickname: nickname, + Affiliation: affiliation, + Role: role, } + + if c.me != nil && senderId == c.me.Id { + myNickname = nickname + myAffiliation = affiliation + myRole = role + continue + } + + c.sendPresence( + gateway.SPFrom(sChatId), + gateway.SPResource(nickname), + gateway.SPImmed(true), + gateway.SPMUCAffiliation(affiliation), + gateway.SPMUCRole(role), + gateway.SPMUCJid(gateway.CHATJID(senderId, true)), + ) } // according to the spec, own member entry should be sent the last From cff81d227cf0c67011b4d35613c0aebcbe6cc777 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 15 Jun 2025 08:37:37 -0400 Subject: [PATCH 137/228] Extract new MUC members from join messages --- telegram/utils.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index b166551..e9d86a3 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1630,17 +1630,25 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { groupChatFrom := "" groupChatTos := []string{} if c.Session.MUC && c.IsGroup(chat) { - if message.Content.MessageContentType() == client.TypeMessageChatDeleteMember { - deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) - c.mucMemberRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) - } - senderId := c.getMessageSenderId(message) if senderId == 0 { log.Errorf("Invalid sender id for message %#v", message) return } + switch message.Content.MessageContentType() { + case client.TypeMessageChatJoinByLink: + c.mucMemberRolePresence(chatId, senderId, ChatMemberStatusUnmuted, c.GetMUCNickname(senderId)) + case client.TypeMessageChatAddMembers: + addMembers, _ := message.Content.(*client.MessageChatAddMembers) + for _, memberId := range addMembers.MemberUserIds { + c.mucMemberRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(memberId)) + } + case client.TypeMessageChatDeleteMember: + deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) + c.mucMemberRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) + } + if !c.mucCacheHasMember(chatId, senderId) { chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ ChatId: chatId, From b1ecfc29cd46b5f93dd79bfd4e4cf20244c4110c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 15 Jun 2025 21:36:03 -0400 Subject: [PATCH 138/228] Support bulk muc#admin operations on occupantsu --- telegram/utils.go | 18 ++- xmpp/extensions/extensions.go | 6 +- xmpp/handlers.go | 212 +++++++++++++++++++++++++--------- 3 files changed, 172 insertions(+), 64 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index e9d86a3..0cae07e 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -85,10 +85,12 @@ const ( type MembersList int const ( - MembersListMembers MembersList = iota + MembersListNone MembersList = iota + MembersListMembers MembersListRestricted MembersListBanned MembersListBannedAndAdministrators + MembersListAdministrators ) const ( @@ -621,9 +623,7 @@ func (c *Client) updateMUCMembers(mucState *MUCState, chatID int64, members []*c myRole := "participant" for _, member := range members { - senderId := c.GetSenderId(member.MemberId) - nickname := c.GetMUCNickname(senderId) - affiliation, role := c.memberStatusToAffiliationAndRole(member.Status) + senderId, nickname, affiliation, role := c.TgMemberToMUCMember(member) mucState.Members[senderId] = &MUCMember{ Nickname: nickname, Affiliation: affiliation, @@ -2485,6 +2485,14 @@ func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMember return "member", "participant" } +// TgMemberToMUCMember resolves useful data to generate a MUC member +func (c *Client) TgMemberToMUCMember(member *client.ChatMember) (senderId int64, nickname, affiliation, role string) { + senderId = c.GetSenderId(member.MemberId) + nickname = c.GetMUCNickname(senderId) + affiliation, role = c.memberStatusToAffiliationAndRole(member.Status) + return +} + func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) { sChatId := gateway.CHATNODE(chatID) var mucJid string @@ -2540,6 +2548,8 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}} case MembersListBannedAndAdministrators: filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}, &client.ChatMembersFilterAdministrators{}} + case MembersListAdministrators: + filters = []client.ChatMembersFilter{&client.ChatMembersFilterAdministrators{}} } limit := int32(9999) diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index d8aafd7..d864493 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -309,9 +309,9 @@ type EmptySubject struct { // QueryMucAdmin is from XEP-0045 type QueryMucAdmin struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/muc#admin query"` - Item QueryMucAdminItem `xml:"item"` - ResultSet *stanza.ResultSet `xml:"set,omitempty"` + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#admin query"` + Items []*QueryMucAdminItem `xml:"item"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` } // QueryMucAdminItem is a child element from XEP-0045 diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 76ed019..34cac24 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -71,6 +71,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetQueryRegister(s, iq) return } + queryMucAdmin, ok := iq.Payload.(*extensions.QueryMucAdmin) + if ok { + go handleGetQueryMucAdmin(s, iq, queryMucAdmin) + return + } } else if iq.Type == stanza.IQTypeSet { queryRegister, ok := iq.Payload.(*extensions.QueryRegister) if ok { @@ -1147,6 +1152,84 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) { } } +func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucAdmin) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + defer gateway.ResumableSend(component, answer) + + bare, _, fromOk := gateway.SplitJID(iq.From) + if !fromOk { + iqAnswerSetError(answer, 400) + return + } + + session, sessionOk := sessions[bare] + if !sessionOk || !session.Session.MUC { + iqAnswerSetError(answer, 403) + return + } + + toID, toOk, toIsGroup := toToID(iq.To) + if !toOk || !toIsGroup { + iqAnswerSetError(answer, 406) + return + } + + if len(query.Items) != 1 { + iqAnswerSetError(answer, 400) + return + } + item := query.Items[0] + + var membersList telegram.MembersList + switch item.Role { + case "moderator": + membersList = telegram.MembersListAdministrators + case "participant": + membersList = telegram.MembersListMembers + } + switch item.Affiliation { + case "owner": + iqAnswerSetError(answer, 403) + return + case "admin": + membersList = telegram.MembersListAdministrators + case "member": + membersList = telegram.MembersListMembers + case "outcast": + membersList = telegram.MembersListBanned + } + + if membersList == telegram.MembersListNone { + iqAnswerSetError(answer, 400) + return + } + + payload := &extensions.QueryMucAdmin{} + answer.Payload = payload + + members, err := session.GetChatMembers(toID, false, "", membersList) + if err == nil { + for _, member := range members { + senderId, nickname, affiliation, role := session.TgMemberToMUCMember(member) + if item.Role != "" && role != item.Role { + continue + } + if item.Affiliation != "" && affiliation != item.Affiliation { + continue + } + payload.Items = append(payload.Items, &extensions.QueryMucAdminItem{ + Jid: gateway.CHATJID(senderId, true), + Nick: nickname, + Role: role, + Affiliation: affiliation, + }) + } + } +} + func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { component, answer, ok := iqResultStub(s, iq) if !ok { @@ -1620,66 +1703,81 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer return } - var userID int64 - if query.Item.Jid != "" { - userID, _, _ = toToID(query.Item.Jid) - } else if query.Item.Nick != "" { - userID = session.GetMUCMemberIdByNickname(toID, query.Item.Nick) - } - if userID == 0 { - iqAnswerSetError(answer, 404) - return + // pre-bake all data to make it transactional as much as possible + type Item struct { + UserID int64 + Nick string + Status telegram.ChatMemberStatus } - nick := query.Item.Nick - if nick == "" { - nick = session.GetMUCNickname(userID) - } - - var status telegram.ChatMemberStatus - var integerPayload int64 - var stringPayload string - - switch query.Item.Role { - case "none": - status = telegram.ChatMemberStatusKicked - case "visitor": - status = telegram.ChatMemberStatusMuted - case "participant": - status = telegram.ChatMemberStatusUnmuted - case "moderator": - status = telegram.ChatMemberStatusPromoted - } - // affiliations have a higher priority over roles - switch query.Item.Affiliation { - case "none": - status = telegram.ChatMemberStatusKicked - case "outcast": - status = telegram.ChatMemberStatusBanned - case "member": - status = telegram.ChatMemberStatusUnmuted - case "admin": - status = telegram.ChatMemberStatusPromoted - case "owner": - iqAnswerSetError(answer, 403) - return - } - - // nothing has been detected - if status == telegram.ChatMemberStatusIllegal { - iqAnswerSetError(answer, 400) - return - } - - err := session.SetChatMemberStatus(toID, userID, status, integerPayload, stringPayload, nick) - if err != nil { - code, ok := telegram.GetErrorCode(err) - if !ok { - code = 500 + var items []Item + for _, item := range query.Items { + if item.Affiliation == "owner" { + iqAnswerSetError(answer, 403) + return + } + + var userID int64 + if item.Jid != "" { + userID, _, _ = toToID(item.Jid) + } else if item.Nick != "" { + userID = session.GetMUCMemberIdByNickname(toID, item.Nick) + } + if userID == 0 { + iqAnswerSetError(answer, 404) + return + } + + nick := session.GetMUCNickname(userID) + + var status telegram.ChatMemberStatus + + switch item.Role { + case "none": + status = telegram.ChatMemberStatusKicked + case "visitor": + status = telegram.ChatMemberStatusMuted + case "participant": + status = telegram.ChatMemberStatusUnmuted + case "moderator": + status = telegram.ChatMemberStatusPromoted + } + // affiliations have a higher priority over roles + switch item.Affiliation { + case "none": + status = telegram.ChatMemberStatusKicked + case "outcast": + status = telegram.ChatMemberStatusBanned + case "member": + status = telegram.ChatMemberStatusUnmuted + case "admin": + status = telegram.ChatMemberStatusPromoted + } + + // nothing has been detected + if status == telegram.ChatMemberStatusIllegal { + iqAnswerSetError(answer, 400) + return + } + + items = append(items, Item{ + UserID: userID, + Nick: nick, + Status: status, + }) + } + + for _, item := range items { + err := session.SetChatMemberStatus(toID, item.UserID, item.Status, 0, "", item.Nick) + if err != nil { + code, ok := telegram.GetErrorCode(err) + if !ok { + code = 500 + } + iqAnswerSetError(answer, int(code)) + answer.Error.Text = err.Error() + return } - iqAnswerSetError(answer, int(code)) - answer.Error.Text = err.Error() - return } } From 67c960da252a50c38ae18ab755aa99cc7d6e7a1e Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 15 Jun 2025 23:21:42 -0400 Subject: [PATCH 139/228] Terminology clarification (member -> occupant wherever applicable) --- telegram/client.go | 8 ++--- telegram/handlers.go | 4 +-- telegram/utils.go | 81 ++++++++++++++++++++++---------------------- xmpp/handlers.go | 4 +-- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index c6acde1..29a8a00 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -24,11 +24,11 @@ type DelayedStatus struct { // MUCState holds MUC metadata type MUCState struct { Resources map[string]bool - Members map[int64]*MUCMember + Occupants map[int64]*MUCOccupant } -// MUCMember represents a MUC member -type MUCMember struct { +// MUCOccupant represents a MUC occupant +type MUCOccupant struct { Nickname string Affiliation string Role string @@ -37,7 +37,7 @@ type MUCMember struct { func NewMUCState() *MUCState { return &MUCState{ Resources: make(map[string]bool), - Members: make(map[int64]*MUCMember), + Occupants: make(map[int64]*MUCOccupant), } } diff --git a/telegram/handlers.go b/telegram/handlers.go index 92e55c9..26f4bcf 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -489,8 +489,8 @@ func (c *Client) updateBasicGroupFullInfo(update *client.UpdateBasicGroupFullInf mucState, ok := c.mucCache[chatID] if ok && mucState != nil { - mucState.Members = make(map[int64]*MUCMember) - c.updateMUCMembers(mucState, chatID, update.BasicGroupFullInfo.Members) + mucState.Occupants = make(map[int64]*MUCOccupant) + c.updateMUCOccupants(mucState, chatID, update.BasicGroupFullInfo.Members) } c.locks.mucCacheLock.Unlock() diff --git a/telegram/utils.go b/telegram/utils.go index 0cae07e..f226303 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -518,14 +518,14 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o c.locks.mucCacheLock.Lock() chatJid := gateway.CHATJID(chatID, true) for mucId, state := range c.mucCache { - member, ok := state.Members[chatID] + occupant, ok := state.Occupants[chatID] if ok { newMucArgs := append( newArgs, gateway.SPFrom(gateway.MUCNODE(mucId)), - gateway.SPResource(member.Nickname), - gateway.SPMUCAffiliation(member.Affiliation), - gateway.SPMUCRole(member.Role), + gateway.SPResource(occupant.Nickname), + gateway.SPMUCAffiliation(occupant.Affiliation), + gateway.SPMUCRole(occupant.Role), gateway.SPMUCJid(chatJid), ) err := c.sendPresence(newMucArgs...) @@ -610,10 +610,10 @@ func (c *Client) sendMUCStatuses(chatID int64) { Limit: 200, Filter: &client.ChatMembersFilterMembers{}, }) - c.updateMUCMembers(mucState, chatID, members.Members) + c.updateMUCOccupants(mucState, chatID, members.Members) } -func (c *Client) updateMUCMembers(mucState *MUCState, chatID int64, members []*client.ChatMember) { +func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []*client.ChatMember) { sChatId := gateway.MUCNODE(chatID) myNickname := "me" if c.me != nil { @@ -623,8 +623,8 @@ func (c *Client) updateMUCMembers(mucState *MUCState, chatID int64, members []*c myRole := "participant" for _, member := range members { - senderId, nickname, affiliation, role := c.TgMemberToMUCMember(member) - mucState.Members[senderId] = &MUCMember{ + senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member) + mucState.Occupants[senderId] = &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, @@ -647,7 +647,7 @@ func (c *Client) updateMUCMembers(mucState *MUCState, chatID int64, members []*c ) } - // according to the spec, own member entry should be sent the last + // according to the spec, own occupant entry should be sent the last c.sendPresence( gateway.SPFrom(sChatId), gateway.SPResource(myNickname), @@ -658,7 +658,7 @@ func (c *Client) updateMUCMembers(mucState *MUCState, chatID int64, members []*c ) } -func (c *Client) mucCacheHasMember(mucID int64, memberID int64) bool { +func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[mucID] @@ -666,11 +666,11 @@ func (c *Client) mucCacheHasMember(mucID int64, memberID int64) bool { return false // no MUC to be added to } - _, ok = mucState.Members[memberID] + _, ok = mucState.Occupants[memberID] return ok } -func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation, role string) bool { +func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role string) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[mucID] @@ -690,7 +690,7 @@ func (c *Client) addMUCMember(mucID int64, memberID int64, affiliation, role str ) if err == nil { - mucState.Members[memberID] = &MUCMember{ + mucState.Occupants[memberID] = &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, @@ -721,7 +721,7 @@ func (c *Client) sendMUCSubject(chatID int64, resource string) { } } -// GetMUCNickname generates a unique nickname for a MUC member +// GetMUCNickname generates a unique nickname for a MUC occupant func (c *Client) GetMUCNickname(chatID int64) string { if chatID == 0 { if c.me != nil { @@ -739,11 +739,12 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { realJid := gateway.CHATJID(memberID, true) for mucId, state := range c.mucCache { - oldMember, ok := state.Members[memberID] + oldOccupant, ok := state.Occupants[memberID] if ok { - state.Members[memberID] = &MUCMember{ + state.Occupants[memberID] = &MUCOccupant{ Nickname: newNickname, - Affiliation: oldMember.Affiliation, + Affiliation: oldOccupant.Affiliation, + Role: oldOccupant.Role, } sMucId := gateway.MUCNODE(mucId) @@ -756,10 +757,10 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { c.sendPresence( gateway.SPType("unavailable"), gateway.SPFrom(sMucId), - gateway.SPResource(oldMember.Nickname), + gateway.SPResource(oldOccupant.Nickname), gateway.SPImmed(true), - gateway.SPMUCAffiliation(oldMember.Affiliation), - gateway.SPMUCRole(oldMember.Role), + gateway.SPMUCAffiliation(oldOccupant.Affiliation), + gateway.SPMUCRole(oldOccupant.Role), gateway.SPMUCNick(newNickname), gateway.SPMUCStatusCodes(unavailableStatusCodes), gateway.SPMUCJid(realJid), @@ -768,8 +769,8 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPFrom(sMucId), gateway.SPResource(newNickname), gateway.SPImmed(true), - gateway.SPMUCAffiliation(oldMember.Affiliation), - gateway.SPMUCRole(oldMember.Role), + gateway.SPMUCAffiliation(oldOccupant.Affiliation), + gateway.SPMUCRole(oldOccupant.Role), gateway.SPMUCStatusCodes(availableStatusCodes), gateway.SPMUCJid(realJid), ) @@ -821,14 +822,14 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { if !ok || mucState == nil { return "", false } - member, ok := mucState.Members[c.me.Id] + occupant, ok := mucState.Occupants[c.me.Id] if !ok { return "", false } - return member.Nickname, true + return occupant.Nickname, true } -// GetMUCMemberIdByNickname looks up the telegram ID by the MUC nickname (slow yet!) +// GetMUCMemberIdByNickname looks up the telegram ID by the MUC nickname (slow yet! (TODO)) func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() @@ -838,8 +839,8 @@ func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { return 0 } - for memberId, member := range mucState.Members { - if member.Nickname == nickname { + for memberId, occupant := range mucState.Occupants { + if occupant.Nickname == nickname { return memberId } } @@ -1638,18 +1639,18 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { switch message.Content.MessageContentType() { case client.TypeMessageChatJoinByLink: - c.mucMemberRolePresence(chatId, senderId, ChatMemberStatusUnmuted, c.GetMUCNickname(senderId)) + c.mucOccupantRolePresence(chatId, senderId, ChatMemberStatusUnmuted, c.GetMUCNickname(senderId)) case client.TypeMessageChatAddMembers: addMembers, _ := message.Content.(*client.MessageChatAddMembers) for _, memberId := range addMembers.MemberUserIds { - c.mucMemberRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(memberId)) + c.mucOccupantRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(memberId)) } case client.TypeMessageChatDeleteMember: deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) - c.mucMemberRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) + c.mucOccupantRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) } - if !c.mucCacheHasMember(chatId, senderId) { + if !c.mucCacheHasOccupant(chatId, senderId) { chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ ChatId: chatId, MemberId: message.SenderId, @@ -1659,7 +1660,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { status = chatMember.Status } affiliation, role := c.memberStatusToAffiliationAndRole(status) - safeToSend = c.addMUCMember(chatId, senderId, affiliation, role) + safeToSend = c.addMUCOccupant(chatId, senderId, affiliation, role) } groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId) @@ -2485,8 +2486,8 @@ func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMember return "member", "participant" } -// TgMemberToMUCMember resolves useful data to generate a MUC member -func (c *Client) TgMemberToMUCMember(member *client.ChatMember) (senderId int64, nickname, affiliation, role string) { +// TgMemberToMUCMember resolves useful data to generate a MUC occupant +func (c *Client) TgMemberToMUCOccupant(member *client.ChatMember) (senderId int64, nickname, affiliation, role string) { senderId = c.GetSenderId(member.MemberId) nickname = c.GetMUCNickname(senderId) affiliation, role = c.memberStatusToAffiliationAndRole(member.Status) @@ -2669,12 +2670,12 @@ func (c *Client) SetChatMemberStatus(chatID, userID int64, status ChatMemberStat Status: chatMemberStatus, }) if err == nil && nickname != "" { - c.mucMemberRolePresence(chatID, userID, status, nickname) + c.mucOccupantRolePresence(chatID, userID, status, nickname) } return err } -func (c *Client) mucMemberRolePresence(chatID, userID int64, status ChatMemberStatus, nickname string) { +func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMemberStatus, nickname string) { args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), gateway.SPResource(nickname), @@ -2723,12 +2724,12 @@ func (c *Client) mucMemberRolePresence(chatID, userID int64, status ChatMemberSt mucState, ok := c.mucCache[chatID] if ok && mucState != nil { if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned { - delete(mucState.Members, userID) + delete(mucState.Occupants, userID) } else { - member, ok := mucState.Members[userID] + occupant, ok := mucState.Occupants[userID] if ok { - member.Affiliation = newAffiliation - member.Role = newRole + occupant.Affiliation = newAffiliation + occupant.Role = newRole } } } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 34cac24..c663cc4 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -161,7 +161,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { if isGroupchat { gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "", msg.Id, 400, true, component) } else { - gateway.SendErrorMessage(msg.From, msg.To, "PMing room members is not supported, use the real JID", 406, true, component) + gateway.SendErrorMessage(msg.From, msg.To, "PMing room occupants is not supported, use the real JID", 406, true, component) } return } @@ -1213,7 +1213,7 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer members, err := session.GetChatMembers(toID, false, "", membersList) if err == nil { for _, member := range members { - senderId, nickname, affiliation, role := session.TgMemberToMUCMember(member) + senderId, nickname, affiliation, role := session.TgMemberToMUCOccupant(member) if item.Role != "" && role != item.Role { continue } From 06a6682bfa4b674f40812b80ab85c24a293af653 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 17 Jun 2025 02:00:42 -0400 Subject: [PATCH 140/228] Make members visitors if message sending is restricted for all in a chat --- telegram/client.go | 1 + telegram/handlers.go | 38 ++++++++++++++++++++++++++++++++++++++ telegram/utils.go | 41 +++++++++++++++++++++++++++++++++-------- xmpp/handlers.go | 4 +++- 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 29a8a00..22ad2b9 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -32,6 +32,7 @@ type MUCOccupant struct { Nickname string Affiliation string Role string + Status client.ChatMemberStatus } func NewMUCState() *MUCState { diff --git a/telegram/handlers.go b/telegram/handlers.go index 26f4bcf..e3a2e0a 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -133,6 +133,9 @@ func (c *Client) updateHandler() { case client.TypeUpdateBasicGroupFullInfo: typedUpdate, _ := update.(*client.UpdateBasicGroupFullInfo) c.updateBasicGroupFullInfo(typedUpdate) + case client.TypeUpdateChatPermissions: + typedUpdate, _ := update.(*client.UpdateChatPermissions) + c.updateChatPermissions(typedUpdate) default: // log only handled types continue @@ -496,3 +499,38 @@ func (c *Client) updateBasicGroupFullInfo(update *client.UpdateBasicGroupFullInf c.locks.mucCacheLock.Unlock() } } + +func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) { + chat, _, _ := c.GetContactByID(update.ChatId, nil) + + // update chat permissions in the cache + if chat != nil { + chat.Permissions = update.Permissions + } + + if c.Session.MUC { + c.locks.mucCacheLock.Lock() + + mucState, ok := c.mucCache[update.ChatId] + if ok && mucState != nil { + for memberID, occupant := range mucState.Occupants { + affiliation, role := c.memberStatusToAffiliationAndRole(occupant.Status, chat) + if affiliation != occupant.Affiliation || role != occupant.Role { + occupant.Affiliation = affiliation + occupant.Role = role + + c.sendPresence( + gateway.SPFrom(gateway.MUCNODE(update.ChatId)), + gateway.SPResource(occupant.Nickname), + gateway.SPImmed(true), + gateway.SPMUCJid(gateway.CHATJID(memberID, true)), + gateway.SPMUCAffiliation(affiliation), + gateway.SPMUCRole(role), + ) + } + } + } + + c.locks.mucCacheLock.Unlock() + } +} diff --git a/telegram/utils.go b/telegram/utils.go index f226303..352dc07 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -622,12 +622,15 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] myAffiliation := "member" myRole := "participant" + chat, _, _ := c.GetContactByID(chatID, nil) + for _, member := range members { - senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member) + senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat) mucState.Occupants[senderId] = &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, + Status: member.Status, } if c.me != nil && senderId == c.me.Id { @@ -670,7 +673,7 @@ func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool { return ok } -func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role string) bool { +func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role string, status client.ChatMemberStatus) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[mucID] @@ -694,6 +697,7 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s Nickname: nickname, Affiliation: affiliation, Role: role, + Status: status, } return true } @@ -745,6 +749,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { Nickname: newNickname, Affiliation: oldOccupant.Affiliation, Role: oldOccupant.Role, + Status: oldOccupant.Status, } sMucId := gateway.MUCNODE(mucId) @@ -1659,8 +1664,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { if err == nil { status = chatMember.Status } - affiliation, role := c.memberStatusToAffiliationAndRole(status) - safeToSend = c.addMUCOccupant(chatId, senderId, affiliation, role) + affiliation, role := c.memberStatusToAffiliationAndRole(status, chat) + safeToSend = c.addMUCOccupant(chatId, senderId, affiliation, role, status) } groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId) @@ -2462,7 +2467,7 @@ func (c *Client) usernamesToString(usernames []string) string { return strings.Join(atUsernames, ", ") } -func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMemberStatus) (string, string) { +func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMemberStatus, chat *client.Chat) (string, string) { if memberStatus != nil { switch memberStatus.ChatMemberStatusType() { case client.TypeChatMemberStatusCreator: @@ -2470,10 +2475,13 @@ func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMember case client.TypeChatMemberStatusAdministrator: return "admin", "moderator" case client.TypeChatMemberStatusMember: + if chat != nil && !c.IsMessageSendingPermitted(chat.Permissions) { + return "member", "visitor" + } return "member", "participant" case client.TypeChatMemberStatusRestricted: restricted, _ := memberStatus.(*client.ChatMemberStatusRestricted) - if restricted.Permissions != nil && !restricted.Permissions.CanSendBasicMessages { + if !c.IsMessageSendingPermitted(restricted.Permissions) { return "member", "visitor" } return "member", "participant" @@ -2487,10 +2495,10 @@ func (c *Client) memberStatusToAffiliationAndRole(memberStatus client.ChatMember } // TgMemberToMUCMember resolves useful data to generate a MUC occupant -func (c *Client) TgMemberToMUCOccupant(member *client.ChatMember) (senderId int64, nickname, affiliation, role string) { +func (c *Client) TgMemberToMUCOccupant(member *client.ChatMember, chat *client.Chat) (senderId int64, nickname, affiliation, role string) { senderId = c.GetSenderId(member.MemberId) nickname = c.GetMUCNickname(senderId) - affiliation, role = c.memberStatusToAffiliationAndRole(member.Status) + affiliation, role = c.memberStatusToAffiliationAndRole(member.Status, chat) return } @@ -2736,6 +2744,23 @@ func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMember c.locks.mucCacheLock.Unlock() } +// IsMessageSendingPermitted evaluates if permissions of the chat allow message sending +func (c *Client) IsMessageSendingPermitted(permissions *client.ChatPermissions) bool { + if permissions == nil { + return true + } + + return permissions.CanSendBasicMessages || + permissions.CanSendAudios || + permissions.CanSendDocuments || + permissions.CanSendPhotos || + permissions.CanSendVideos || + permissions.CanSendVideoNotes || + permissions.CanSendVoiceNotes || + permissions.CanSendPolls || + permissions.CanSendOtherMessages +} + // GetErrorCode obtains an error code from a Telegram response error func GetErrorCode(err error) (int32, bool) { responseError, ok := err.(client.ResponseError) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index c663cc4..450ad9d 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1212,8 +1212,10 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer members, err := session.GetChatMembers(toID, false, "", membersList) if err == nil { + chat, _, _ := session.GetContactByID(toID, nil) + for _, member := range members { - senderId, nickname, affiliation, role := session.TgMemberToMUCOccupant(member) + senderId, nickname, affiliation, role := session.TgMemberToMUCOccupant(member, chat) if item.Role != "" && role != item.Role { continue } From 746a537cb961e55916515144670806351e45ea4d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 17 Jun 2025 02:07:34 -0400 Subject: [PATCH 141/228] Correct room features according to the fact visitor role is used --- xmpp/handlers.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 450ad9d..2e11af5 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -930,8 +930,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { "http://jabber.org/protocol/muc", "muc_persistent", "muc_hidden", - "muc_membersonly", - "muc_unmoderated", + "muc_moderated", "muc_nonanonymous", "muc_unsecured", "http://jabber.org/protocol/muc#stable_id", From 174037e63f90bf31d36183e67b645a7c56a02cc7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 17 Jun 2025 03:04:43 -0400 Subject: [PATCH 142/228] Return 405 for non-existent room JIDs as this should be considered a room creation attempt --- xmpp/handlers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 2e11af5..7b1f41e 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -561,7 +561,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen chatId, ok, toIsGroup := toToID(toBare) if !ok || !toIsGroup { - presenceReplySetError(reply, 404) + presenceReplySetError(reply, 405) return } @@ -579,7 +579,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen chat, _, err := session.GetContactByID(chatId, nil) if err != nil || !session.IsGroup(chat) { - presenceReplySetError(reply, 404) + presenceReplySetError(reply, 405) return } @@ -1862,9 +1862,9 @@ func presenceReplySetError(reply *stanza.Presence, code int) { case 407: reply.Error.Type = stanza.ErrorTypeAuth reply.Error.Reason = "registration-required" - case 404: + case 405: reply.Error.Type = stanza.ErrorTypeCancel - reply.Error.Reason = "item-not-found" + reply.Error.Reason = "not-allowed" default: log.Error("Unknown error code, falling back with empty reason") reply.Error.Type = stanza.ErrorTypeCancel From cef56ca484dfee2f242fe4f9fb7b973f3f22edea Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 17 Jun 2025 03:40:55 -0400 Subject: [PATCH 143/228] Reject reserved MUC creation --- xmpp/extensions/extensions.go | 22 ++++++++++++++++++++++ xmpp/handlers.go | 14 ++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index d864493..c8889b2 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -324,6 +324,12 @@ type QueryMucAdminItem struct { Reason string `xml:"reason,omitempty"` } +// QueryMucOwner is from XEP-0045 +type QueryMucOwner struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#owner query"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -419,6 +425,16 @@ func (c QueryMucAdmin) GetSet() *stanza.ResultSet { return c.ResultSet } +// Namespace is a namespace! +func (c QueryMucOwner) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c QueryMucOwner) GetSet() *stanza.ResultSet { + return c.ResultSet +} + // NewReplyFallback initializes a fallback range func NewReplyFallback(start uint64, end uint64) Fallback { return Fallback{ @@ -546,4 +562,10 @@ func init() { "http://jabber.org/protocol/muc#admin", "query", }, QueryMucAdmin{}) + + // muc owner query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "http://jabber.org/protocol/muc#owner", + "query", + }, QueryMucOwner{}) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 7b1f41e..d0c3416 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -76,6 +76,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetQueryMucAdmin(s, iq, queryMucAdmin) return } + _, ok = iq.Payload.(*extensions.QueryMucOwner) + if ok { + go handleGetQueryMucOwner(s, iq) + return + } } else if iq.Type == stanza.IQTypeSet { queryRegister, ok := iq.Payload.(*extensions.QueryRegister) if ok { @@ -1231,6 +1236,15 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } } +func handleGetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + iqAnswerSetError(answer, 405) + gateway.ResumableSend(component, answer) +} + func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { component, answer, ok := iqResultStub(s, iq) if !ok { From f18fded3e9c91f77572cce1a4246f6ea5ea5a713 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 18 Jun 2025 01:46:53 -0400 Subject: [PATCH 144/228] Room configuration form --- telegram/utils.go | 47 ++++++++ xmpp/extensions/extensions.go | 1 + xmpp/handlers.go | 201 +++++++++++++++++++++++++++++++++- 3 files changed, 244 insertions(+), 5 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 352dc07..76f8abd 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2683,6 +2683,53 @@ func (c *Client) SetChatMemberStatus(chatID, userID int64, status ChatMemberStat return err } +// SetChatTitle is a handy wrapper for the following TDLib method +func (c *Client) SetChatTitle(chatID int64, title string) error { + _, err := c.client.SetChatTitle(&client.SetChatTitleRequest{ + ChatId: chatID, + Title: title, + }) + return err +} + +// SetChatDescription is a handy wrapper for the following TDLib method +func (c *Client) SetChatDescription(chatID int64, description string) error { + _, err := c.client.SetChatDescription(&client.SetChatDescriptionRequest{ + ChatId: chatID, + Description: description, + }) + return err +} + +// SetChatPermissions is a handy wrapper for the following TDLib method +func (c *Client) SetChatPermissions(chatID int64, permissions *client.ChatPermissions) error { + _, err := c.client.SetChatPermissions(&client.SetChatPermissionsRequest{ + ChatId: chatID, + Permissions: permissions, + }) + return err +} + +// CloneChatPermissions makes a copy of ChatPermissions structure +func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermissions { + return &client.ChatPermissions{ + CanSendBasicMessages: permissions.CanSendBasicMessages, + CanSendAudios: permissions.CanSendAudios, + CanSendDocuments: permissions.CanSendDocuments, + CanSendPhotos: permissions.CanSendPhotos, + CanSendVideos: permissions.CanSendVideos, + CanSendVideoNotes: permissions.CanSendVideoNotes, + CanSendVoiceNotes: permissions.CanSendVoiceNotes, + CanSendPolls: permissions.CanSendPolls, + CanSendOtherMessages: permissions.CanSendOtherMessages, + CanAddWebPagePreviews: permissions.CanAddWebPagePreviews, + CanChangeInfo: permissions.CanChangeInfo, + CanInviteUsers: permissions.CanInviteUsers, + CanPinMessages: permissions.CanPinMessages, + CanManageTopics: permissions.CanManageTopics, + } +} + func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMemberStatus, nickname string) { args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index c8889b2..432b882 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -327,6 +327,7 @@ type QueryMucAdminItem struct { // QueryMucOwner is from XEP-0045 type QueryMucOwner struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#owner query"` + Form *stanza.Form `xml:"jabber:x:data x` ResultSet *stanza.ResultSet `xml:"set,omitempty"` } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index d0c3416..fee1cf4 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -26,6 +26,8 @@ const ( TypeVCard4 ) +const MUC_DEFAULT_HISTORY_LIMIT int32 = 20 + func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) } @@ -97,6 +99,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleSetQueryMucAdmin(s, iq, queryMucAdmin) return } + queryMucOwner, ok := iq.Payload.(*extensions.QueryMucOwner) + if ok { + go handleSetQueryMucOwner(s, iq, queryMucOwner) + return + } } else if iq.Type == stanza.IQTypeResult { discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { @@ -603,7 +610,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen } else if !mucExt.History.Since.IsZero() { limit = telegram.NewMessageLimitSince(mucExt.History.Since.Unix()) } else { - limit = telegram.NewMessageLimitMessages(20) + limit = telegram.NewMessageLimitMessages(MUC_DEFAULT_HISTORY_LIMIT) } session.JoinMUC(chatId, fromResource, limit) } @@ -1181,6 +1188,12 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer return } + chat, _, err := session.GetContactByID(toID, nil) + if err != nil || !session.IsGroup(chat) { + iqAnswerSetError(answer, 405) + return + } + if len(query.Items) != 1 { iqAnswerSetError(answer, 400) return @@ -1216,8 +1229,6 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer members, err := session.GetChatMembers(toID, false, "", membersList) if err == nil { - chat, _, _ := session.GetContactByID(toID, nil) - for _, member := range members { senderId, nickname, affiliation, role := session.TgMemberToMUCOccupant(member, chat) if item.Role != "" && role != item.Role { @@ -1241,8 +1252,69 @@ func handleGetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ) { if !ok { return } - iqAnswerSetError(answer, 405) - gateway.ResumableSend(component, answer) + defer gateway.ResumableSend(component, answer) + + bare, _, fromOk := gateway.SplitJID(iq.From) + if !fromOk { + iqAnswerSetError(answer, 400) + return + } + + session, sessionOk := sessions[bare] + if !sessionOk || !session.Session.MUC { + iqAnswerSetError(answer, 403) + return + } + + toID, toOk, toIsGroup := toToID(iq.To) + if !toOk || !toIsGroup { + iqAnswerSetError(answer, 405) + return + } + + chat, _, err := session.GetContactByID(toID, nil) + if err != nil || chat == nil || !session.IsGroup(chat) { + iqAnswerSetError(answer, 405) + return + } + + payload := &extensions.QueryMucOwner{} + answer.Payload = payload + + dummyString := "" + changeSubject := "1" + if chat.Permissions != nil && !chat.Permissions.CanPinMessages { + changeSubject = "0" + } + + payload.Form = &stanza.Form{ + Type: stanza.FormTypeForm, + Title: fmt.Sprintf("Configuration for \"%v\" room", chat.Title), + Fields: []*stanza.Field{ + &stanza.Field{ + Var: "FORM_TYPE", + Type: stanza.FieldTypeHidden, + ValuesList: []string{"http://jabber.org/protocol/muc#roominfo"}, + }, + &stanza.Field{ + Var: "muc#roomconfig_roomname", + Label: "Group name", + Required: &dummyString, + ValuesList: []string{chat.Title}, + }, + &stanza.Field{ + Var: "muc#roomconfig_roomdesc", + Label: "Description (optional)", + ValuesList: []string{session.GetChatDescription(chat)}, + }, + &stanza.Field{ + Var: "muc#roomconfig_changesubject", + Label: "Allow Occupants to Change Subject?", + Type: stanza.FieldTypeBool, + ValuesList: []string{changeSubject}, + }, + }, + } } func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { @@ -1796,6 +1868,113 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } } +func handleSetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucOwner) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + defer gateway.ResumableSend(component, answer) + + bare, _, fromOk := gateway.SplitJID(iq.From) + if !fromOk { + iqAnswerSetError(answer, 400) + return + } + + session, sessionOk := sessions[bare] + if !sessionOk || !session.Session.MUC { + iqAnswerSetError(answer, 403) + return + } + + toID, toOk, toIsGroup := toToID(iq.To) + if !toOk || !toIsGroup { + iqAnswerSetError(answer, 405) + return + } + + chat, _, err := session.GetContactByID(toID, nil) + if err != nil || chat == nil || !session.IsGroup(chat) { + iqAnswerSetError(answer, 405) + return + } + + if query.Form == nil { + iqAnswerSetError(answer, 400) + return + } + + switch query.Form.Type { + case stanza.FormTypeSubmit: + // okay, noop + case stanza.FormTypeCancel: + return + default: + iqAnswerSetError(answer, 400) + return + } + + err = nil + for _, field := range query.Form.Fields { + switch field.Var { + case "muc#roomconfig_roomname", "muc#roomconfig_roomdesc", "muc#roomconfig_changesubject": + if len(field.ValuesList) != 1 { + iqAnswerSetError(answer, 400) + return + } + + value := field.ValuesList[0] + + switch field.Var { + case "muc#roomconfig_roomname": + if value == "" { + iqAnswerSetError(answer, 400) + return + } + if value != chat.Title { + err = session.SetChatTitle(toID, value) + if err != nil { + break + } + } + case "muc#roomconfig_roomdesc": + if value != session.GetChatDescription(chat) { + err = session.SetChatDescription(toID, value) + if err != nil { + break + } + } + case "muc#roomconfig_changesubject": + b, ok := ToBool(value) + if !ok { + iqAnswerSetError(answer, 400) + return + } + permissions := chat.Permissions + if permissions != nil && b != permissions.CanPinMessages { + newPermissions := telegram.CloneChatPermissions(permissions) + newPermissions.CanPinMessages = b + + err = session.SetChatPermissions(toID, newPermissions) + if err != nil { + break + } + } + } + } + } + + if err != nil { + code, ok := telegram.GetErrorCode(err) + if !ok { + code = 500 + } + iqAnswerSetError(answer, int(code)) + answer.Error.Text = err.Error() + return + } +} + func iqAnswerSetError(answer *stanza.IQ, code int) { iqAnswerSetErrorInternal(answer, code, false) } @@ -2124,3 +2303,15 @@ func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *tel return nil } + +// ToBool returns bool, ok +func ToBool(b string) (bool, bool) { + switch b { + case "0", "false": + return false, true + case "1", "true": + return true, true + } + + return false, false +} From 0b1e204912f4979de26f648c8fdafd3db3992c81 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 22 Jun 2025 10:37:20 -0400 Subject: [PATCH 145/228] Process MUC deletion --- telegram/commands.go | 4 +- telegram/utils.go | 41 +++++++++++++ xmpp/extensions/extensions.go | 11 +++- xmpp/gateway/gateway.go | 64 +++++++++++++------- xmpp/handlers.go | 110 ++++++++++++++++++---------------- 5 files changed, 152 insertions(+), 78 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index b43f2f8..d78cd20 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -980,9 +980,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } // leave current chat (for owners) case "leave!": - _, err := c.client.DeleteChat(&client.DeleteChatRequest{ - ChatId: chatID, - }) + err := c.DeleteChat(chatID) if err != nil { return err.Error(), true, false } diff --git a/telegram/utils.go b/telegram/utils.go index 76f8abd..80ff4b3 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -588,6 +588,39 @@ func (c *Client) LeaveMUC(chatId int64, resource string) { } } +// DestroyMUC removes everyone from the MUC +func (c *Client) DestroyMUC(chatId int64) error { + err := c.DeleteChat(chatId) + if err != nil { + return err + } + + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + + mucState, ok := c.mucCache[chatId] + if !ok || mucState == nil { + return nil + } + + var toResources []string + for resource := range mucState.Resources { + toResources = append(toResources, resource) + } + c.sendPresence( + gateway.SPFrom(gateway.MUCNODE(chatId)), + gateway.SPResource(c.GetMUCNickname(0)), + gateway.SPToResources(toResources), + gateway.SPMUCAffiliation("none"), + gateway.SPMUCRole("none"), + gateway.SPMUCDestroy(""), + ) + + delete(c.mucCache, chatId) + + return nil +} + func (c *Client) getFullName(user *client.User) string { fullName := user.FirstName if user.LastName != "" { @@ -2710,6 +2743,14 @@ func (c *Client) SetChatPermissions(chatID int64, permissions *client.ChatPermis return err } +// DeleteChat is a handy wrapper for the following TDLib method +func (c *Client) DeleteChat(chatID int64) error { + _, err := c.client.DeleteChat(&client.DeleteChatRequest{ + ChatId: chatID, + }) + return err +} + // CloneChatPermissions makes a copy of ChatPermissions structure func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermissions { return &client.ChatPermissions{ diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 432b882..b1f9634 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -244,6 +244,7 @@ type MessageXMucUserInviteContinue struct { type PresenceXMucUserExtension struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` Item PresenceXMucUserItem + Destroy *MucDestroy Statuses []PresenceXMucUserStatus } @@ -327,10 +328,18 @@ type QueryMucAdminItem struct { // QueryMucOwner is from XEP-0045 type QueryMucOwner struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#owner query"` - Form *stanza.Form `xml:"jabber:x:data x` + Form *stanza.Form `xml:"jabber:x:data x,omitempty"` + Destroy *MucDestroy `xml:"destroy,omitempty"` ResultSet *stanza.ResultSet `xml:"set,omitempty"` } +// MucDestroy is a child element from XEP-0045 +type MucDestroy struct { + XMLName xml.Name `xml:"destroy"` + Jid string `xml:"jid,attr,omitempty"` + Reason string `xml:"reason,omitempty"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 604a1eb..777c5eb 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -450,6 +450,12 @@ var SPMUCJid = args.NewString() // SPMUCStatusCodes is a set of XEP-0045 MUC status codes var SPMUCStatusCodes = args.New() +// SPMUCDestroy is a XEP-0045 room destruction element +var SPMUCDestroy = args.NewString() + +// SPToResources achieves to send the presence to certain resources only +var SPToResources = args.New() + func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { var presenceFrom string if SPFrom.IsSet(args) { @@ -534,6 +540,12 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { }) } } + if SPMUCDestroy.IsSet(args) { + userExt.Destroy = &extensions.MucDestroy{ + Jid: SPMUCDestroy.Get(args), + Reason: "Group was deleted", + } + } presence.Extensions = append(presence.Extensions, userExt) } } @@ -557,29 +569,39 @@ func SendPresence(component *xmpp.Component, to string, args ...args.V) error { "to": to, }).Info("Got presence") - presence := newPresence(bareJid, to, args...) - - // explicit check, as marshalling is expensive - if log.GetLevel() == log.DebugLevel { - xmlPresence, err := xml.Marshal(presence) - if err == nil { - log.Debug(string(xmlPresence)) - } else { - log.Debugf("%#v", presence) - } - } - - immed := SPImmed.Get(args) - if immed { - err := ResumableSend(component, presence) - if err != nil { - LogBadPresence(&presence) - return err + var tos []string + if SPToResources.IsSet(args) { + for _, toResource := range SPToResources.Get(args).([]string) { + tos = append(tos, to + "/" + toResource) } } else { - QueueLock.Lock() - Queue[presence.From+presence.To] = &presence - QueueLock.Unlock() + tos = []string{to} + } + for _, to := range tos { + presence := newPresence(bareJid, to, args...) + + // explicit check, as marshalling is expensive + if log.GetLevel() == log.DebugLevel { + xmlPresence, err := xml.Marshal(presence) + if err == nil { + log.Debug(string(xmlPresence)) + } else { + log.Debugf("%#v", presence) + } + } + + immed := SPImmed.Get(args) + if immed { + err := ResumableSend(component, presence) + if err != nil { + LogBadPresence(&presence) + return err + } + } else { + QueueLock.Lock() + Queue[presence.From+presence.To] = &presence + QueueLock.Unlock() + } } return nil diff --git a/xmpp/handlers.go b/xmpp/handlers.go index fee1cf4..38a0f5c 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1899,69 +1899,72 @@ func handleSetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer return } - if query.Form == nil { - iqAnswerSetError(answer, 400) - return - } - - switch query.Form.Type { - case stanza.FormTypeSubmit: - // okay, noop - case stanza.FormTypeCancel: - return - default: - iqAnswerSetError(answer, 400) - return - } - - err = nil - for _, field := range query.Form.Fields { - switch field.Var { - case "muc#roomconfig_roomname", "muc#roomconfig_roomdesc", "muc#roomconfig_changesubject": - if len(field.ValuesList) != 1 { - iqAnswerSetError(answer, 400) - return - } - - value := field.ValuesList[0] + if query.Form != nil { + switch query.Form.Type { + case stanza.FormTypeSubmit: + // okay, noop + case stanza.FormTypeCancel: + return + default: + iqAnswerSetError(answer, 400) + return + } + err = nil + for _, field := range query.Form.Fields { switch field.Var { - case "muc#roomconfig_roomname": - if value == "" { + case "muc#roomconfig_roomname", "muc#roomconfig_roomdesc", "muc#roomconfig_changesubject": + if len(field.ValuesList) != 1 { iqAnswerSetError(answer, 400) return } - if value != chat.Title { - err = session.SetChatTitle(toID, value) - if err != nil { - break - } - } - case "muc#roomconfig_roomdesc": - if value != session.GetChatDescription(chat) { - err = session.SetChatDescription(toID, value) - if err != nil { - break - } - } - case "muc#roomconfig_changesubject": - b, ok := ToBool(value) - if !ok { - iqAnswerSetError(answer, 400) - return - } - permissions := chat.Permissions - if permissions != nil && b != permissions.CanPinMessages { - newPermissions := telegram.CloneChatPermissions(permissions) - newPermissions.CanPinMessages = b - err = session.SetChatPermissions(toID, newPermissions) - if err != nil { - break + value := field.ValuesList[0] + + switch field.Var { + case "muc#roomconfig_roomname": + if value == "" { + iqAnswerSetError(answer, 400) + return + } + if value != chat.Title { + err = session.SetChatTitle(toID, value) + if err != nil { + break + } + } + case "muc#roomconfig_roomdesc": + if value != session.GetChatDescription(chat) { + err = session.SetChatDescription(toID, value) + if err != nil { + break + } + } + case "muc#roomconfig_changesubject": + b, ok := ToBool(value) + if !ok { + iqAnswerSetError(answer, 400) + return + } + permissions := chat.Permissions + if permissions != nil && b != permissions.CanPinMessages { + newPermissions := telegram.CloneChatPermissions(permissions) + newPermissions.CanPinMessages = b + + err = session.SetChatPermissions(toID, newPermissions) + if err != nil { + break + } } } } } + } else if query.Destroy != nil { + err = session.DestroyMUC(toID) + } else { + // per 1.0 spec version, it also could be a destruction, but too dangerous to cover probably + iqAnswerSetError(answer, 400) + return } if err != nil { @@ -1973,6 +1976,7 @@ func handleSetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer answer.Error.Text = err.Error() return } + } func iqAnswerSetError(answer *stanza.IQ, code int) { From 863c5afa911a647acc74a8e56aee4a79a6a21975 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 23 Jun 2025 11:50:59 -0400 Subject: [PATCH 146/228] Kick from MUCs on transport shutdown --- telegram/connect.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/telegram/connect.go b/telegram/connect.go index d37c5fd..b332352 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -234,6 +234,21 @@ func (c *Client) Disconnect(resource string, quit bool) bool { c.sendPresence(args...) } + if c.Session.MUC { + c.locks.mucCacheLock.Lock() + for chatID := range c.mucCache { + c.sendPresence( + gateway.SPFrom(gateway.MUCNODE(chatID)), + gateway.SPResource(c.GetMUCNickname(0)), + gateway.SPType("unavailable"), + gateway.SPMUCAffiliation("none"), + gateway.SPMUCRole("none"), + gateway.SPMUCStatusCodes([]uint16{110, 332}), + ) + } + c.locks.mucCacheLock.Unlock() + } + c.close() return true From 32b00c244c3fc3c67cac0ff1a1eafc1ad4ccac0b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 23 Jun 2025 11:54:48 -0400 Subject: [PATCH 147/228] Avoid empty jid attribute in MUC user extension item --- xmpp/extensions/extensions.go | 2 +- xmpp/gateway/gateway.go | 10 ++++++++-- xmpp/handlers.go | 31 ++++++++++--------------------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index b1f9634..e6e2f10 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -252,7 +252,7 @@ type PresenceXMucUserExtension struct { type PresenceXMucUserItem struct { XMLName xml.Name `xml:"item"` Affiliation string `xml:"affiliation,attr"` - Jid string `xml:"jid,attr"` + Jid *string `xml:"jid,attr"` Nick string `xml:"nick,attr,omitempty"` Role string `xml:"role,attr"` } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 777c5eb..a305ac3 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -414,6 +414,9 @@ func LogBadPresence(presence *stanza.Presence) { // SPFrom is a Telegram user id var SPFrom = args.NewString() +// SPFullFrom is for specifying a full from when desired +var SPFullFrom = args.NewString() + // SPType is a presence type var SPType = args.NewString() @@ -458,7 +461,9 @@ var SPToResources = args.New() func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { var presenceFrom string - if SPFrom.IsSet(args) { + if SPFullFrom.IsSet(args) { + presenceFrom = SPFullFrom.Get(args) + } else if SPFrom.IsSet(args) { presenceFrom = SPFrom.Get(args) + "@" + bareJid if SPResource.IsSet(args) { resource := SPResource.Get(args) @@ -530,7 +535,8 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { userExt.Item.Nick = SPMUCNick.Get(args) } if SPMUCJid.IsSet(args) { - userExt.Item.Jid = SPMUCJid.Get(args) + mucJid := SPMUCJid.Get(args) + userExt.Item.Jid = &mucJid } if SPMUCStatusCodes.IsSet(args) { statusCodes := SPMUCStatusCodes.Get(args).([]uint16) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 38a0f5c..9e3eac8 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -698,27 +698,16 @@ func handleMUCUnavailable(component *xmpp.Component, p stanza.Presence, session session.LeaveMUC(chatId, resource) - reply := &stanza.Presence{ - Attrs: stanza.Attrs{ - From: p.To, - To: p.From, - Id: p.Id, - Type: stanza.PresenceTypeUnavailable, - }, - Extensions: []stanza.PresExtension{ - extensions.PresenceXMucUserExtension{ - Item: extensions.PresenceXMucUserItem{ - Affiliation: "member", - Jid: p.From, - Role: "none", - }, - Statuses: []extensions.PresenceXMucUserStatus{ - extensions.PresenceXMucUserStatus{Code: 110}, - }, - }, - }, - } - gateway.ResumableSend(component, reply) + gateway.SendPresence( + component, + p.From, + gateway.SPFullFrom(p.To), + gateway.SPType("unavailable"), + gateway.SPMUCAffiliation("member"), + gateway.SPMUCRole("none"), + gateway.SPMUCJid(p.From), + gateway.SPMUCStatusCodes([]uint16{110}), + ) } func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { From 93dcc32480c64bdc1117d7765ba8fcca65beda1d Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 24 Jun 2025 11:18:44 -0400 Subject: [PATCH 148/228] Fix error handling for non-existent MUCs --- xmpp/handlers.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 9e3eac8..afb5027 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -591,7 +591,7 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen chat, _, err := session.GetContactByID(chatId, nil) if err != nil || !session.IsGroup(chat) { - presenceReplySetError(reply, 405) + presenceReplySetError(reply, 404) return } @@ -913,6 +913,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { defer gateway.ResumableSend(component, answer) disco := answer.DiscoInfo() + answer.Payload = disco toID, toOk, toIsGroup := toToID(iq.To) if di.Node == "" { @@ -969,7 +970,12 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { } if toOk { - if !isMuc { + if toIsGroup { + if !isMuc { + iqAnswerSetError(answer, 404) + return + } + } else { disco.AddIdentity("", "account", "registered") } disco.AddFeatures(stanza.NSMsgChatMarkers) @@ -1014,7 +1020,6 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { } } } - answer.Payload = disco } func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { @@ -2045,12 +2050,15 @@ func presenceReplySetError(reply *stanza.Presence, code int) { case 400: reply.Error.Type = stanza.ErrorTypeModify reply.Error.Reason = "jid-malformed" - case 407: - reply.Error.Type = stanza.ErrorTypeAuth - reply.Error.Reason = "registration-required" + case 404: + reply.Error.Type = stanza.ErrorTypeCancel + reply.Error.Reason = "item-not-found" case 405: reply.Error.Type = stanza.ErrorTypeCancel reply.Error.Reason = "not-allowed" + case 407: + reply.Error.Type = stanza.ErrorTypeAuth + reply.Error.Reason = "registration-required" default: log.Error("Unknown error code, falling back with empty reason") reply.Error.Type = stanza.ErrorTypeCancel From 6d24823bb312ae922119c4f3236d9c3fcbd8f7f2 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 25 Jun 2025 13:14:54 -0400 Subject: [PATCH 149/228] Assure all MUC presences have affiliation/role/realJID --- telegram/connect.go | 5 +++++ telegram/utils.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/telegram/connect.go b/telegram/connect.go index b332352..9eca5cc 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -236,6 +236,10 @@ func (c *Client) Disconnect(resource string, quit bool) bool { if c.Session.MUC { c.locks.mucCacheLock.Lock() + var myJid string + if c.me != nil { + myJid = gateway.CHATJID(c.me.Id, true) + } for chatID := range c.mucCache { c.sendPresence( gateway.SPFrom(gateway.MUCNODE(chatID)), @@ -243,6 +247,7 @@ func (c *Client) Disconnect(resource string, quit bool) bool { gateway.SPType("unavailable"), gateway.SPMUCAffiliation("none"), gateway.SPMUCRole("none"), + gateway.SPMUCJid(myJid), gateway.SPMUCStatusCodes([]uint16{110, 332}), ) } diff --git a/telegram/utils.go b/telegram/utils.go index 80ff4b3..bf99046 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -607,12 +607,17 @@ func (c *Client) DestroyMUC(chatId int64) error { for resource := range mucState.Resources { toResources = append(toResources, resource) } + var myJid string + if c.me != nil { + myJid = gateway.CHATJID(c.me.Id, true) + } c.sendPresence( gateway.SPFrom(gateway.MUCNODE(chatId)), gateway.SPResource(c.GetMUCNickname(0)), gateway.SPToResources(toResources), gateway.SPMUCAffiliation("none"), gateway.SPMUCRole("none"), + gateway.SPMUCJid(myJid), gateway.SPMUCDestroy(""), ) @@ -649,8 +654,10 @@ func (c *Client) sendMUCStatuses(chatID int64) { func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []*client.ChatMember) { sChatId := gateway.MUCNODE(chatID) myNickname := "me" + var myJid string if c.me != nil { myNickname = c.getFullName(c.me) + myJid = gateway.CHATJID(c.me.Id, true) } myAffiliation := "member" myRole := "participant" @@ -690,6 +697,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] gateway.SPImmed(true), gateway.SPMUCAffiliation(myAffiliation), gateway.SPMUCRole(myRole), + gateway.SPMUCJid(myJid), gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), ) } From de74787a48f8221750b36e5dfe114d076ca4e306 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 26 Jun 2025 11:28:04 -0400 Subject: [PATCH 150/228] Setting both role and affiliation should yield bad-request --- xmpp/handlers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index afb5027..d21c394 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1813,6 +1813,11 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer var status telegram.ChatMemberStatus + if item.Role != "" && item.Affiliation != "" { + iqAnswerSetError(answer, 400) + return + } + switch item.Role { case "none": status = telegram.ChatMemberStatusKicked @@ -1823,7 +1828,6 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer case "moderator": status = telegram.ChatMemberStatusPromoted } - // affiliations have a higher priority over roles switch item.Affiliation { case "none": status = telegram.ChatMemberStatusKicked From 3b57895237a033827c9220b70e9af4982fa9c0f5 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 27 Jun 2025 12:57:14 -0400 Subject: [PATCH 151/228] Apply restrictions on muc#admin IQs --- xmpp/handlers.go | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index d21c394..f1d4052 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1194,6 +1194,11 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } item := query.Items[0] + if item.Role != "" && item.Affiliation != "" { + iqAnswerSetError(answer, 400) + return + } + var membersList telegram.MembersList switch item.Role { case "moderator": @@ -1231,12 +1236,18 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer if item.Affiliation != "" && affiliation != item.Affiliation { continue } - payload.Items = append(payload.Items, &extensions.QueryMucAdminItem{ - Jid: gateway.CHATJID(senderId, true), - Nick: nickname, - Role: role, - Affiliation: affiliation, - }) + newItem := extensions.QueryMucAdminItem{} + if item.Affiliation == "" || item.Affiliation != "outcast" { + newItem.Nick = nickname + } + if item.Role != "" { + newItem.Role = role + newItem.Jid = gateway.CHATJID(senderId, true) + } else { + newItem.Affiliation = affiliation + newItem.Jid = gateway.CHATJID(senderId, false) + } + payload.Items = append(payload.Items, &newItem) } } } @@ -1798,6 +1809,14 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer return } + if (item.Role != "" && item.Affiliation != "") || + (item.Role != "" && item.Nick == "") || + (item.Affiliation != "" && item.Jid == "") { + iqAnswerSetError(answer, 400) + return + } + + var userID int64 if item.Jid != "" { userID, _, _ = toToID(item.Jid) @@ -1813,11 +1832,6 @@ func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer var status telegram.ChatMemberStatus - if item.Role != "" && item.Affiliation != "" { - iqAnswerSetError(answer, 400) - return - } - switch item.Role { case "none": status = telegram.ChatMemberStatusKicked From 541acdf37288f4eadf8b7d2dd3df7e6458653a3a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 27 Jun 2025 15:05:32 -0400 Subject: [PATCH 152/228] Explicit muc#traffic support --- xmpp/handlers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index f1d4052..0e4b893 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -999,6 +999,8 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { } } } + } else if di.Node == "http://jabber.org/protocol/muc#traffic" { + // noop yet, empty result as intended, TODO: add XHTML whenever supported } else { chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) From 06645eaa8c1f1a1d70892d0d78d8913dd9f11ce7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 27 Jun 2025 15:43:29 -0400 Subject: [PATCH 153/228] Return bad-request for disco attempts on MUC occupants --- xmpp/handlers.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 0e4b893..206e93e 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -916,6 +916,14 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { answer.Payload = disco toID, toOk, toIsGroup := toToID(iq.To) + if toIsGroup { + toJid, err := stanza.NewJid(iq.To) + if err == nil && toJid.Resource != "" { + iqAnswerSetError(answer, 400) + return + } + } + if di.Node == "" { var isMuc bool bare, _, fromOk := gateway.SplitJID(iq.From) @@ -1033,7 +1041,15 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { log.Debugf("discoItems: %#v", di) - toID, toOk, _ := toToID(iq.To) + toID, toOk, toIsGroup := toToID(iq.To) + + if toIsGroup { + toJid, err := stanza.NewJid(iq.To) + if err == nil && toJid.Resource != "" { + iqAnswerSetError(answer, 400) + return + } + } disco := answer.DiscoItems() From d5c1d6ff2a7679da3e9f02bb7a60fcfe80e529de Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 28 Jun 2025 07:49:04 -0400 Subject: [PATCH 154/228] Return 403 on join if banned from a group --- telegram/utils.go | 33 +++++++++++++++++++++++++++++++++ xmpp/handlers.go | 7 +++++++ 2 files changed, 40 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index bf99046..109e16d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2857,6 +2857,39 @@ func (c *Client) IsMessageSendingPermitted(permissions *client.ChatPermissions) permissions.CanSendOtherMessages } +// GetMyStatusInChat checks the membership status of current account in the given chat +func (c *Client) GetMyStatusInChat(chatID int64) ChatMemberStatus { + if c.me == nil { + return ChatMemberStatusIllegal + } + + member, err := c.client.GetChatMember(&client.GetChatMemberRequest{ + ChatId: chatID, + MemberId: &client.MessageSenderUser{UserId: c.me.Id}, + }) + if err != nil { + return ChatMemberStatusIllegal + } + return c.getChatMemberStatus(member.Status) +} + +func (c *Client) getChatMemberStatus(status client.ChatMemberStatus) ChatMemberStatus { + switch status.ChatMemberStatusType() { + case client.TypeChatMemberStatusCreator, client.TypeChatMemberStatusAdministrator: + return ChatMemberStatusPromoted + case client.TypeChatMemberStatusMember: + return ChatMemberStatusUnbanned + case client.TypeChatMemberStatusRestricted: + return ChatMemberStatusMuted + case client.TypeChatMemberStatusLeft: + return ChatMemberStatusKicked + case client.TypeChatMemberStatusBanned: + return ChatMemberStatusBanned + } + + return ChatMemberStatusIllegal +} + // GetErrorCode obtains an error code from a Telegram response error func GetErrorCode(err error) (int32, bool) { responseError, ok := err.(client.ResponseError) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 206e93e..e18f98b 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -595,6 +595,13 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen return } + status := session.GetMyStatusInChat(chatId) + // TODO: seems to be impossible for basic groups, check back when supergroups are supported + if status == telegram.ChatMemberStatusBanned { + presenceReplySetError(reply, 403) + return + } + log.Debugf("%#v", mucExt) maxStanzas, maxStanzasOk := mucExt.History.MaxStanzas.Get() maxChars, maxCharsOk := mucExt.History.MaxChars.Get() From ab9d041cef7b8eb6da405bd34aa002e8e37066ac Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 29 Jun 2025 11:37:36 -0400 Subject: [PATCH 155/228] Reject groupchat 1.0 join attempts --- xmpp/handlers.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index e18f98b..ac0e11e 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -657,16 +657,26 @@ func tryHandleMUCPresence(s xmpp.Sender, p stanza.Presence) { return } - if !session.MUCHasResource(chatId, fromResource) { - return - } - component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } + if !session.MUCHasResource(chatId, fromResource) { + // groupchat 1.0 join + gateway.SendPresence( + component, + p.From, + gateway.SPFullFrom(p.To), + gateway.SPType("unavailable"), + gateway.SPMUCAffiliation("none"), + gateway.SPMUCRole("none"), + gateway.SPMUCStatusCodes([]uint16{110, 307, 333}), + ) + return + } + switch p.Type { case "": handleMUCNicknameChange(component, p, session, chatId, toBare) From a01e864d718daf37eb64e5e3bbd00b09f63add02 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 29 Jun 2025 17:01:16 -0400 Subject: [PATCH 156/228] Advertise MUC avatars via XEP-0486 --- telegram/client.go | 6 ++--- telegram/utils.go | 44 ++++++++++++++++++++++++++++++----- xmpp/extensions/extensions.go | 13 ++++++++--- xmpp/gateway/gateway.go | 43 ++++++++++++++++++++++------------ xmpp/handlers.go | 38 +++++++++++------------------- 5 files changed, 93 insertions(+), 51 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 22ad2b9..524ade3 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -91,8 +91,8 @@ type Client struct { XmppClientFeatures map[string]*[]string XmppClientFeaturesLock sync.Mutex - AvatarHashes map[int64]*HashedAvatar - AvatarHashesLock sync.Mutex + avatarHashes map[int64]*HashedAvatar + avatarHashesLock sync.Mutex locks clientLocks SendMessageLock sync.Mutex @@ -182,7 +182,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component lastMsgHashes: make(map[int64]uint64), lastMsgIds: make(map[int64]string), XmppClientFeatures: make(map[string]*[]string), - AvatarHashes: make(map[int64]*HashedAvatar), + avatarHashes: make(map[int64]*HashedAvatar), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), }, diff --git a/telegram/utils.go b/telegram/utils.go index 109e16d..e041412 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -408,12 +408,12 @@ func (c *Client) getFileData(tgFile *client.File, typ byte) string { // 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{ + c.avatarHashesLock.Lock() + c.avatarHashes[chatId] = &HashedAvatar{ Hash: "", File: 0, } - c.AvatarHashesLock.Unlock() + c.avatarHashesLock.Unlock() } // GetPhotoSize return at least a rough size @@ -431,12 +431,12 @@ func (c *Client) GetPhotoSize(photo *client.File) int64 { // 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{ + c.avatarHashesLock.Lock() + c.avatarHashes[chatId] = &HashedAvatar{ Hash: sha1, File: photo.Id, } - c.AvatarHashesLock.Unlock() + c.avatarHashesLock.Unlock() return sha1 } @@ -445,6 +445,32 @@ func (c *Client) GetPhotoBase64(photo *client.File) string { return c.getFileData(photo, typeFileDataBase64) } +// GetHashedAvatar obtain the avatar hash from cache or requests the avatar file immediately to calculate it +func (c *Client) GetHashedAvatar(chatId int64) *HashedAvatar { + c.avatarHashesLock.Lock() + hashedAvatar, ok := c.avatarHashes[chatId] + c.avatarHashesLock.Unlock() + + if !ok { + log.Info("Could not find avatar in cache, fetching immediately") + + chat, _, err := c.GetContactByID(chatId, nil) + if err != nil || chat == nil || chat.Photo == nil { + return nil + } + + file := chat.Photo.Small + + sha1 := c.GetPhotoSha1(file, chatId) + hashedAvatar = &HashedAvatar{ + Hash: sha1, + File: file.Id, + } + } + + return hashedAvatar +} + // ProcessStatusUpdate sets contact status func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, oldArgs ...args.V) error { if !c.Online() { @@ -1776,6 +1802,12 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id } } } + + if isGroupchat { + for _, jid := range jids { + gateway.SendMUCStatusCode(jid, gateway.MUCJID(chatId), c.xmpp, 104) + } + } } } } else { diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index e6e2f10..04d13d2 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -221,9 +221,10 @@ type MessageXLegacyInviteExtension struct { // MessageXMucUserExtension is from XEP-0045 type MessageXMucUserExtension struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` - Invite MessageXMucUserInvite - Password string `xml:"password,omitempty"` + XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` + Invite *MessageXMucUserInvite `xml:"invite,omitempty"` + Status *MessageXMucUserStatus `xml:"status,omitempty"` + Password string `xml:"password,omitempty"` } // MessageXMucUserInvite is from XEP-0045 @@ -240,6 +241,12 @@ type MessageXMucUserInviteContinue struct { Thread string `xml:"thread,attr,omitempty"` } +// MessageXMucUserStatus is from XEP-0486 +type MessageXMucUserStatus struct { + XMLName xml.Name `xml:"status"` + Code string `xml:"code,attr"` +} + // PresenceXMucUserExtension is from XEP-0045 type PresenceXMucUserExtension struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index a305ac3..c6de4f8 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -89,7 +89,7 @@ func MUCJID(chatId int64) string { // SendMessage creates and sends a message stanza func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId) + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) } // SendServiceMessage creates and sends a simple message stanza from transport @@ -98,7 +98,7 @@ func SendServiceMessage(to, body string, component *xmpp.Component) { if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "") + sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "", 0) } // SendTextMessage creates and sends a simple message stanza @@ -107,27 +107,27 @@ func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupch if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "") + sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "", 0) } // SendErrorMessage creates and sends an error message stanza func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "") + sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0) } // SendErrorMessageWithBody creates and sends an error message stanza with body payload func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "") + sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0) } // SendMessageWithOOB creates and sends a message stanza with OOB URL func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId) + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) } // SendSubjectMessage creates and sends a MUC subject func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { - sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "", "") + sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "", "", 0) } // SendMessageMarker creates and sends a message stanza with a XEP-0333 marker @@ -135,15 +135,20 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker sendMessageWrapper(to, from, "", "", "", "", component, nil, &marker{ Type: markerType, Id: markerId, - }, 0, "", "", false, false, false, false, "", 0, "", "") + }, 0, "", "", false, false, false, false, "", 0, "", "", 0) } // SendMUCInvite creates and send a MUC invitation message func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) { - sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "") + sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "", 0) } -func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string) { +// SendMUCStatusCode creates a groupchat message with a muc#user status code +func SendMUCStatusCode(to string, from string, component *xmpp.Component, statusCode int64) { + sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", statusCode) +} + +func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string, statusCode int64) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -297,15 +302,23 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if replaceId != "" { message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId}) } + var userExt extensions.MessageXMucUserExtension if inviteFrom != "" { - message.Extensions = append(message.Extensions, extensions.MessageXMucUserExtension{ - Invite: extensions.MessageXMucUserInvite{ - From: inviteFrom, - }, - }, extensions.MessageXLegacyInviteExtension{ + userExt.Invite = &extensions.MessageXMucUserInvite{ + From: inviteFrom, + } + message.Extensions = append(message.Extensions, extensions.MessageXLegacyInviteExtension{ Jid: messageFrom, }) } + if statusCode != 0 { + userExt.Status = &extensions.MessageXMucUserStatus{ + Code: strconv.FormatInt(statusCode, 10), + } + } + if inviteFrom != "" || statusCode != 0 { + message.Extensions = append(message.Extensions, userExt) + } if stanzaId != "" { message.Extensions = append(message.Extensions, extensions.MessageStanzaId{ Id: stanzaId, diff --git a/xmpp/handlers.go b/xmpp/handlers.go index ac0e11e..8a8bc51 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -745,12 +745,12 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { return } - toParts := strings.Split(iq.To, "@") - toID, err := strconv.ParseInt(toParts[0], 10, 64) - if err != nil { + toID, toOk, _ := toToID(iq.To) + if !toOk { log.Error("Invalid IQ to") return } + info, err := session.GetVcardInfo(toID) if err != nil { log.Error(err) @@ -826,27 +826,7 @@ func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGe 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() - } + hashedAvatar := session.GetHashedAvatar(chatId) if id != "" && hashedAvatar.Hash != id { log.Infof("Cache contains %v hash for chat %v, but %v was requested; aborting", hashedAvatar.Hash, iq.To, id) @@ -963,6 +943,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { "http://jabber.org/protocol/muc#stable_id", "jabber:iq:register", "urn:xmpp:sid:0", + "vcard-temp", ) fields := []*stanza.Field{ &stanza.Field{ @@ -982,6 +963,15 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { }, } + hashedAvatar := session.GetHashedAvatar(toID) + if hashedAvatar != nil { + fields = append(fields, &stanza.Field{ + Var: "muc#roominfo_avatarhash", + Label: "Avatar hash", + ValuesList: []string{hashedAvatar.Hash}, + }) + } + disco.Form = stanza.NewForm(fields, "result") } } else if !toOk { From 4b2ae50f6734063127bdddffca26af60def9dddb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 30 Jun 2025 10:44:54 -0400 Subject: [PATCH 157/228] Allow listing MUC owners --- telegram/utils.go | 49 ++++++++++++++++++++++++++++++++++++++++++++--- xmpp/handlers.go | 3 +-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index e041412..618ca6f 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -91,6 +91,7 @@ const ( MembersListBanned MembersListBannedAndAdministrators MembersListAdministrators + MembersListCreators ) const ( @@ -221,6 +222,18 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli return chat, user, nil } +// GetChatByID gets exactly a chat from a cache, or error if chat is not found +func (c *Client) GetChatByID(id int64, chat *client.Chat) (*client.Chat, error) { + chat, _, err := c.GetContactByID(id, nil) + if err != nil { + return nil, err + } else if chat == nil { + return nil, errors.New("Chat not found") + } + + return chat, nil +} + // GetChatType obtains chat type from its information func (c *Client) GetChatType(id int64) (ChatType, *client.Chat, error) { if !c.Online() || id == 0 { @@ -2620,6 +2633,38 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p // 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) { + if membersList == MembersListCreators { + chat, err := c.GetChatByID(chatID, nil) + if err != nil { + return nil, err + } + + 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 fullInfo.CreatorUserId != 0 { + chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ + ChatId: chatID, + MemberId: &client.MessageSenderUser{UserId: fullInfo.CreatorUserId}, + }) + if err != nil { + return nil, err + } + + return []*client.ChatMember{chatMember}, nil + } + } + + return nil, errors.New("Creator not found") + } + var filters []client.ChatMembersFilter switch membersList { case MembersListMembers: @@ -2638,11 +2683,9 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member if limited { limit = 20 - chat, _, err := c.GetContactByID(chatID, nil) + chat, err := c.GetChatByID(chatID, nil) if err != nil { return nil, err - } else if chat == nil { - return nil, errors.New("Chat not found") } chatType := chat.Type.ChatTypeType() diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 8a8bc51..576b533 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1233,8 +1233,7 @@ func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } switch item.Affiliation { case "owner": - iqAnswerSetError(answer, 403) - return + membersList = telegram.MembersListCreators case "admin": membersList = telegram.MembersListAdministrators case "member": From 35ec6d44cd5c2e5e32bc84ac9b051695ce89bbd7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 30 Jun 2025 20:48:30 -0400 Subject: [PATCH 158/228] Adapt avatar hash updates for MUCs --- telegram/utils.go | 76 +++++++++++++++++++++++++---------------- xmpp/gateway/gateway.go | 6 ++-- xmpp/handlers.go | 8 ++++- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 618ca6f..8f27279 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -499,8 +499,13 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o return err } + var isMUC bool if chat != nil && c.Session.MUC && c.IsGroup(chat) { - return nil + // allow MUC presence hack for avatars, still discard the rest + if status != "" || show != "" { + return nil + } + isMUC = true } var photo string @@ -513,36 +518,39 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o presenceType = gateway.SPType.Get(oldArgs) } - cachedStatus, ok := c.cache.GetStatus(chatID) - if status == "" { - if ok { - var typ string - show, status, typ = cachedStatus.Destruct() - if presenceType == "" { - presenceType = typ + // skip cache for MUCs + if !isMUC { + cachedStatus, ok := c.cache.GetStatus(chatID) + if status == "" { + if ok { + var typ string + show, status, typ = cachedStatus.Destruct() + if presenceType == "" { + presenceType = typ + } + log.WithFields(log.Fields{ + "show": show, + "status": status, + "presenceType": presenceType, + }).Debug("Cached status") + } else if user != nil && user.Status != nil { + show, status, presenceType = c.userStatusToText(user.Status, chatID) + log.WithFields(log.Fields{ + "show": show, + "status": status, + "presenceType": presenceType, + }).Debug("Status to text") + } else { + show, status = "chat", chat.Title } - log.WithFields(log.Fields{ - "show": show, - "status": status, - "presenceType": presenceType, - }).Debug("Cached status") - } else if user != nil && user.Status != nil { - show, status, presenceType = c.userStatusToText(user.Status, chatID) - log.WithFields(log.Fields{ - "show": show, - "status": status, - "presenceType": presenceType, - }).Debug("Status to text") - } else { - show, status = "chat", chat.Title } - } - cacheShow := show - if presenceType == "unavailable" { - cacheShow = presenceType + cacheShow := show + if presenceType == "unavailable" { + cacheShow = presenceType + } + c.cache.SetStatus(chatID, cacheShow, status) } - c.cache.SetStatus(chatID, cacheShow, status) newArgs := []args.V{ gateway.SPShow(show), @@ -576,7 +584,11 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o } c.locks.mucCacheLock.Unlock() - newArgs = gateway.SPAppendFrom(newArgs, chatID) + if isMUC { + newArgs = append(newArgs, gateway.SPFullFrom(gateway.MUCJID(chatID))) + } else { + newArgs = gateway.SPAppendFrom(newArgs, chatID) + } return c.sendPresence(newArgs...) } @@ -1809,7 +1821,13 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id if ok && features != nil { for _, feature := range *features { if feature == gateway.NodeAvatarMetadataNotify { - go gateway.SendPubSubAvatarNotification(c.xmpp, c.jid+"/"+resource, chatId, sha1, size) + var chatJid string + if isGroupchat { + chatJid = gateway.MUCJID(chatId) + } else { + chatJid = gateway.CHATJID(chatId, false) + } + go gateway.SendPubSubAvatarNotification(c.xmpp, c.jid+"/"+resource, chatJid, sha1, size) break } } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index c6de4f8..bb3d9de 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -686,7 +686,7 @@ func affiliationToRole(affilation string) string { } // SendPubSubAvatarNotification encourages clients to fetch an avatar -func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId int64, sha1 string, size int64) { +func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatJid string, sha1 string, size int64) { info := stanza.Node{ XMLName: xml.Name{Local: "info"}, Attrs: []xml.Attr{ @@ -698,7 +698,7 @@ func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId }, } log.WithFields(log.Fields{ - "chatId": chatId, + "chatJid": chatJid, }).Debugf("%#v", info) event := &stanza.PubSubEvent{ @@ -718,7 +718,7 @@ func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatId message := stanza.Message{ Attrs: stanza.Attrs{ - From: CHATJID(chatId, false), + From: chatJid, To: jid, Type: stanza.MessageTypeHeadline, }, diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 576b533..b41e3ad 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2193,7 +2193,13 @@ func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram. sha1 := session.GetPhotoSha1(chat.Photo.Small, chat.Id) size := session.GetPhotoSize(chat.Photo.Small) - gateway.SendPubSubAvatarNotification(component, jid, chat.Id, sha1, size) + var chatJid string + if session.Session.MUC && session.IsGroup(chat) { + chatJid = gateway.MUCJID(chat.Id) + } else { + chatJid = gateway.CHATJID(chat.Id, false) + } + gateway.SendPubSubAvatarNotification(component, jid, chatJid, sha1, size) } } From da83ee38bd5853411b9f9c8b04eec8a918ed740c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 1 Jul 2025 12:24:28 -0400 Subject: [PATCH 159/228] Care about delivering message deletions to every resource --- telegram/handlers.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index e3a2e0a..7bc3afc 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -403,13 +403,19 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { deleteChar = "✗ " } text := deleteChar + strings.Join(int64SliceToStringSlice(update.MessageIds), ",") + var fromJid string + var jids []string if isGroupchat { fromJid = gateway.MUCJID(update.ChatId) + _, jids = c.getMUCJoinedJIDs(update.ChatId) } else { fromJid = gateway.CHATNODE(update.ChatId) + c.getCarbonFullJids(true, "") + } + for _, jid := range jids { + gateway.SendTextMessage(jid, fromJid, text, c.xmpp, isGroupchat) } - gateway.SendTextMessage(c.jid, fromJid, text, c.xmpp, isGroupchat) } } From d665dbdcc44b3188393f4cf6ed9b65889b1bdf70 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 1 Jul 2025 15:35:34 -0400 Subject: [PATCH 160/228] Fix command responses in MUCs --- telegram/utils.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index 8f27279..8ec51c4 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2056,7 +2056,11 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) { if isGroupchat { - gateway.SendErrorMessage(returnJid, gateway.MUCJID(chatID), text, code, isGroupchat, c.xmpp) + if code != 0 { + gateway.SendErrorMessage(returnJid, gateway.MUCJID(chatID), text, code, isGroupchat, c.xmpp) + } else { + gateway.SendTextMessage(returnJid, gateway.MUCJID(chatID), text, c.xmpp, isGroupchat) + } } else { gateway.SendTextMessage(returnJid, gateway.CHATNODE(chatID), text, c.xmpp, isGroupchat) } From 2b5bb43bdd664715fff776a1b92d23db59c7cbec Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 1 Jul 2025 16:01:43 -0400 Subject: [PATCH 161/228] Send an invite to MUC on /add wherever applicable --- telegram/commands.go | 2 +- telegram/handlers.go | 6 +++--- telegram/utils.go | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index d78cd20..d4d8d37 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -1126,7 +1126,7 @@ func (c *Client) cmdAdd(args []string) (string, bool) { return "No error, but chat is nil", false } - c.subscribeToID(chat.Id, chat) + c.subscribeToID(chat.Id, chat, true) return "", true } diff --git a/telegram/handlers.go b/telegram/handlers.go index 7bc3afc..dfbc8f0 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -180,7 +180,7 @@ func (c *Client) updateNewChat(update *client.UpdateNewChat) { c.cache.SetChat(update.Chat.Id, update.Chat) if update.Chat.Positions != nil && len(update.Chat.Positions) > 0 { - c.subscribeToID(update.Chat.Id, update.Chat) + c.subscribeToID(update.Chat.Id, update.Chat, false) } if update.Chat.Id < 0 { @@ -192,14 +192,14 @@ func (c *Client) updateNewChat(update *client.UpdateNewChat) { // chat position is updated func (c *Client) updateChatPosition(update *client.UpdateChatPosition) { if update.Position != nil && update.Position.Order != 0 { - go c.subscribeToID(update.ChatId, nil) + go c.subscribeToID(update.ChatId, nil, false) } } // chat last message is updated func (c *Client) updateChatLastMessage(update *client.UpdateChatLastMessage) { if update.Positions != nil && len(update.Positions) > 0 { - go c.subscribeToID(update.ChatId, nil) + go c.subscribeToID(update.ChatId, nil, false) } } diff --git a/telegram/utils.go b/telegram/utils.go index 8ec51c4..30f95c4 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2401,7 +2401,7 @@ func (c *Client) IsGroup(chat *client.Chat) bool { } // subscribe to a Telegram ID -func (c *Client) subscribeToID(id int64, chat *client.Chat) { +func (c *Client) subscribeToID(id int64, chat *client.Chat, firstTime bool) { args := gateway.SimplePresence(id, "subscribe") if chat == nil { @@ -2409,6 +2409,11 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) { } if chat != nil { if c.Session.MUC && c.IsGroup(chat) { + if firstTime { + for resource := range c.resourcesRange() { + gateway.InviteToMUC(id, c.jid+"/"+resource, c.xmpp) + } + } return } From 9721e60b0a92e336b6fea7869aeb851bcee6328b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 2 Jul 2025 12:55:58 -0400 Subject: [PATCH 162/228] Send a MUC invitation when a new group is created --- telegram/utils.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index 30f95c4..331ccf9 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1745,6 +1745,10 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { case client.TypeMessageChatDeleteMember: deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) c.mucOccupantRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) + case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate: + for resource := range c.resourcesRange() { + gateway.InviteToMUC(chatId, c.jid+"/"+resource, c.xmpp) + } } if !c.mucCacheHasOccupant(chatId, senderId) { From 65cc9b1551592d65cc5c7da7d26e3d77f44df2a0 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 3 Jul 2025 13:47:22 -0400 Subject: [PATCH 163/228] Kick from MUCs instead of unsubscribing in commands --- telegram/commands.go | 15 ++++------- telegram/connect.go | 14 +--------- telegram/utils.go | 63 ++++++++++++++++++++++++++++++++------------ 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index d4d8d37..bbf71ba 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -275,11 +275,6 @@ func keyValueString(key, value string) string { return fmt.Sprintf("%s: %s", key, value) } -func (c *Client) unsubscribe(chatID int64) error { - args := gateway.SimplePresence(chatID, "unsubscribed") - return c.sendPresence(args...) -} - func (c *Client) usernameOrIDToID(username string) (int64, error) { userID, err := strconv.ParseInt(username, 10, 64) // couldn't parse the id, try to lookup as a username @@ -361,7 +356,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } for _, id := range c.cache.ChatsKeys() { - c.unsubscribe(id) + c.leaveChat(id) } c.Session.Login = "" @@ -974,7 +969,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return err.Error(), true, false } - err = c.unsubscribe(chatID) + err = c.leaveChat(chatID) if err != nil { return err.Error(), true, false } @@ -985,7 +980,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return err.Error(), true, false } - err = c.unsubscribe(chatID) + err = c.leaveChat(chatID) if err != nil { return err.Error(), true, false } @@ -1027,7 +1022,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return err.Error(), true, false } - err = c.unsubscribe(chatID) + err = c.leaveChat(chatID) if err != nil { return err.Error(), true, false } @@ -1043,7 +1038,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return err.Error(), true, false } - err = c.unsubscribe(chatID) + err = c.leaveChat(chatID) if err != nil { return err.Error(), true, false } diff --git a/telegram/connect.go b/telegram/connect.go index 9eca5cc..f78f964 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -236,20 +236,8 @@ func (c *Client) Disconnect(resource string, quit bool) bool { if c.Session.MUC { c.locks.mucCacheLock.Lock() - var myJid string - if c.me != nil { - myJid = gateway.CHATJID(c.me.Id, true) - } for chatID := range c.mucCache { - c.sendPresence( - gateway.SPFrom(gateway.MUCNODE(chatID)), - gateway.SPResource(c.GetMUCNickname(0)), - gateway.SPType("unavailable"), - gateway.SPMUCAffiliation("none"), - gateway.SPMUCRole("none"), - gateway.SPMUCJid(myJid), - gateway.SPMUCStatusCodes([]uint16{110, 332}), - ) + c.kickMeFromMUC(chatID, []uint16{110, 332}, false, nil) } c.locks.mucCacheLock.Unlock() } diff --git a/telegram/utils.go b/telegram/utils.go index 331ccf9..abf89d8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -654,23 +654,7 @@ func (c *Client) DestroyMUC(chatId int64) error { return nil } - var toResources []string - for resource := range mucState.Resources { - toResources = append(toResources, resource) - } - var myJid string - if c.me != nil { - myJid = gateway.CHATJID(c.me.Id, true) - } - c.sendPresence( - gateway.SPFrom(gateway.MUCNODE(chatId)), - gateway.SPResource(c.GetMUCNickname(0)), - gateway.SPToResources(toResources), - gateway.SPMUCAffiliation("none"), - gateway.SPMUCRole("none"), - gateway.SPMUCJid(myJid), - gateway.SPMUCDestroy(""), - ) + c.kickMeFromMUC(chatId, nil, true, mucState) delete(c.mucCache, chatId) @@ -2767,6 +2751,51 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member return members, nil } +func (c *Client) unsubscribe(chatID int64) error { + args := gateway.SimplePresence(chatID, "unsubscribed") + return c.sendPresence(args...) +} + +func (c *Client) leaveChat(chatID int64) error { + chat, err := c.GetChatByID(chatID, nil) + + if err == nil && c.Session.MUC && c.IsGroup(chat) { + return c.kickMeFromMUC(chatID, []uint16{110, 307}, false, nil) + } + return c.unsubscribe(chatID) +} + +func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, mucState *MUCState) error { + var myJid string + if c.me != nil { + myJid = gateway.CHATJID(c.me.Id, true) + } + args := []args.V{ + gateway.SPFrom(gateway.MUCNODE(chatID)), + gateway.SPResource(c.GetMUCNickname(0)), + gateway.SPMUCAffiliation("none"), + gateway.SPMUCRole("none"), + gateway.SPMUCJid(myJid), + gateway.SPMUCStatusCodes(statusCodes), + } + if destroy { + var toResources []string + if mucState != nil { + for resource := range mucState.Resources { + toResources = append(toResources, resource) + } + } + args = append( + args, + gateway.SPMUCDestroy(""), + gateway.SPToResources(toResources), + ) + } else { + args = append(args, gateway.SPType("unavailable")) + } + return c.sendPresence(args...) +} + // MigrateToMUCs unsubscribes from legacy group chats and invites to MUCs func (c *Client) MigrateToMUCs() { var chatIDs []int64 From d6582a888fec32376b1b9213e59d0fa7668816b5 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 3 Jul 2025 13:48:37 -0400 Subject: [PATCH 164/228] Add MUC-to-legacy migrator --- telegram/commands.go | 9 +++++++-- telegram/utils.go | 16 ++++++++++++++++ xmpp/handlers.go | 9 +++++++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index bbf71ba..ad4e066 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -458,8 +458,13 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin if err != nil { return err.Error(), false } - if args[0] == "muc" && args[1] == "true" { - go c.MigrateToMUCs() + if args[0] == "muc" { + switch args[1] { + case "true": + go c.MigrateToMUCs() + case "false": + go c.MigrateFromMUCs() + } } gateway.DirtySessions = true diff --git a/telegram/utils.go b/telegram/utils.go index abf89d8..4f59d0d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2817,6 +2817,22 @@ func (c *Client) MigrateToMUCs() { } } +// MigrateFromMUCs kicks from MUCs and subscribes back to legacy group chats +func (c *Client) MigrateFromMUCs() { + var chatIDs []int64 + for _, chat := range c.GetGroupChats() { + chatIDs = append(chatIDs, chat.Id) + } + + c.locks.mucCacheLock.Lock() + for _, chatID := range chatIDs { + c.kickMeFromMUC(chatID, []uint16{110, 332}, false, nil) + delete(c.mucCache, chatID) + c.subscribeToID(chatID, nil, false) + } + c.locks.mucCacheLock.Unlock() +} + // SetChatMemberStatus is a handy wrapper for the following TDLib method func (c *Client) SetChatMemberStatus(chatID, userID int64, status ChatMemberStatus, numericPayload int64, stringPayload, nickname string) error { var chatMemberStatus client.ChatMemberStatus diff --git a/xmpp/handlers.go b/xmpp/handlers.go index b41e3ad..b218323 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1478,8 +1478,13 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command errString = fmt.Sprintf("Error for field %v: %v, aborting", field.Var, err.Error()) break } - if field.Var == "muc" && fieldValue == "true" { - go session.MigrateToMUCs() + if field.Var == "muc" { + switch fieldValue { + case "true": + go session.MigrateToMUCs() + case "false": + go session.MigrateFromMUCs() + } } infoStrings = append(infoStrings, fmt.Sprintf("%s set to %s", field.Var, value)) gateway.DirtySessions = true From 5c4c722cdc446ad8cb50c8610fdb0a006ff7c051 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 4 Jul 2025 16:08:11 -0400 Subject: [PATCH 165/228] Ping back outgoing commands in MUCs --- telegram/utils.go | 31 +++++++++++++++---------------- xmpp/handlers.go | 7 +++++-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 4f59d0d..abfd394 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -932,7 +932,7 @@ func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { // NewPinnedMessage sends a text message and pins it right away func (c *Client) NewPinnedMessage(chatID int64, text, returnJid string) bool { c.locks.pinOutboxLock.Lock() - msg := c.ProcessOutgoingMessage(chatID, text, returnJid, 0, 0, true, true) + msg, _ := c.ProcessOutgoingMessage(chatID, text, returnJid, 0, 0, true, true) if msg == nil { c.locks.pinOutboxLock.Unlock() return false @@ -1755,13 +1755,12 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { safeToSend = false } } + log.Debugf("groupChatFrom: %v groupChatTos: %#v, safeToSend: %v", groupChatFrom, groupChatTos, safeToSend) if safeToSend { c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos) } else { mucJID := gateway.MUCJID(chatId) - for _, to := range groupChatTos { - gateway.SendErrorMessage(to, mucJID, "Cannot show a message", 500, true, c.xmpp) - } + gateway.SendErrorMessage(c.jid, mucJID, "Cannot show a message", 500, true, c.xmpp) } } @@ -1935,11 +1934,11 @@ func (c *Client) PrepareOutgoingMessageContent(text string) client.InputMessageC return c.prepareOutgoingMessageContent(text, nil) } -// 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, isGroupchat, raw bool) *client.Message { +// ProcessOutgoingMessage executes commands or sends messages to mapped chats, returns message id and isCommand +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64, replaceId int64, isGroupchat, raw bool) (*client.Message, bool) { if !c.Online() { // we're offline - return nil + return nil, false } if replaceId == 0 && !raw && (strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!")) { @@ -1950,7 +1949,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } // do not send on success if isCommand { - return nil + return nil, true } } @@ -1979,24 +1978,24 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str if response.StatusCode != 200 { c.returnMessage(returnJid, chatID, fmt.Sprintf("Received status code %v", response.StatusCode), response.StatusCode, isGroupchat) - return nil + return nil, false } tempDir, err := ioutil.TempDir("", "telegabber-*") if err != nil { c.returnError(returnJid, chatID, "Failed to create a temporary directory", err, 500, isGroupchat) - return nil + return nil, false } tempFile, err := os.Create(filepath.Join(tempDir, filepath.Base(text))) if err != nil { c.returnError(returnJid, chatID, "Failed to create a temporary file", err, 500, isGroupchat) - return nil + return nil, false } _, err = io.Copy(tempFile, response.Body) if err != nil { c.returnError(returnJid, chatID, "Failed to write a temporary file", err, 500, isGroupchat) - return nil + return nil, false } file = &client.InputFileLocal{ @@ -2025,9 +2024,9 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str }) if err != nil { c.returnError(returnJid, chatID, "Not edited", err, 400, isGroupchat) - return nil + return nil, false } - return tgMessage + return tgMessage, false } tgMessage, err := c.client.SendMessage(&client.SendMessageRequest{ @@ -2037,9 +2036,9 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str }) if err != nil { c.returnError(returnJid, chatID, "Not sent", err, 400, isGroupchat) - return nil + return nil, false } - return tgMessage + return tgMessage, false } func (c *Client) returnMessage(returnJid string, chatID int64, text string, code int, isGroupchat bool) { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index b218323..dceee3a 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -264,7 +264,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { session.SendMessageLock.Lock() defer session.SendMessageLock.Unlock() - tgMessage := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, isGroupchat, false) + tgMessage, isCommand := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, isGroupchat, false) if tgMessage != nil { if replaceId != 0 { // not needed (is it persistent among clients though?) @@ -294,6 +294,9 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { []string{msg.From}, ) } + } else if isCommand && isGroupchat && session.Session.MUC { + // pong outgoing commands back to groupchats + gateway.SendMessage(msg.From, msg.To + "/" + session.GetMUCNickname(0), text, "", component, nil, 0, "", false, isGroupchat, false, "", "") } else { /* // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway @@ -1540,7 +1543,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command if ok { msgText := "/" + form.Fields[0].ValuesList[0] session.LastBotCmdString = msgText - tgMessage := session.ProcessOutgoingMessage(toId, msgText, iq.From, 0, 0, false, true) + tgMessage, _ := session.ProcessOutgoingMessage(toId, msgText, iq.From, 0, 0, false, true) if tgMessage != nil { payload.Status = stanza.CommandStatusCompleted } else { From 3b01b1de700a42071b751a073707c92731aca3e7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 5 Jul 2025 12:43:38 -0400 Subject: [PATCH 166/228] Support /history command in MUCs --- telegram/handlers.go | 4 +- telegram/utils.go | 109 +++++++++++++++++++++++++++---------------- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index dfbc8f0..3369496 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -274,7 +274,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { var jids []string if isMUC { - _, jids = c.getMUCJoinedJIDs(update.ChatId) + _, jids = c.getMUCJoinedJIDs(update.ChatId, nil, true) } else { c.getCarbonFullJids(true, ignoredResource) } @@ -408,7 +408,7 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { var jids []string if isGroupchat { fromJid = gateway.MUCJID(update.ChatId) - _, jids = c.getMUCJoinedJIDs(update.ChatId) + _, jids = c.getMUCJoinedJIDs(update.ChatId, nil, true) } else { fromJid = gateway.CHATNODE(update.ChatId) c.getCarbonFullJids(true, "") diff --git a/telegram/utils.go b/telegram/utils.go index abfd394..c5d7274 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -872,19 +872,23 @@ func (c *Client) MUCHasResource(chatID int64, resource string) bool { return ok } -func (c *Client) getMUCJoinedJIDs(chatId int64) (bool, []string) { - c.locks.mucCacheLock.Lock() - defer c.locks.mucCacheLock.Unlock() +func (c *Client) getMUCJoinedJIDs(chatId int64, mucState *MUCState, lock bool) (bool, []string) { + if lock { + c.locks.mucCacheLock.Lock() + defer c.locks.mucCacheLock.Unlock() + } groupChatTos := []string{} - mucState, ok := c.mucCache[chatId] - if !ok || mucState == nil { + if mucState == nil { + mucState, _ = c.mucCache[chatId] + } + if mucState == nil { return false, nil - } else { - for resource := range mucState.Resources { - groupChatTos = append(groupChatTos, c.jid + "/" + resource) - } + } + + for resource := range mucState.Resources { + groupChatTos = append(groupChatTos, c.jid + "/" + resource) } return true, groupChatTos @@ -1099,7 +1103,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten return } -func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, message *client.Message) string { +func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, sender bool, message *client.Message) string { var err error if message == nil { message, err = c.client.GetMessage(&client.GetMessageRequest{ @@ -1115,16 +1119,18 @@ func (c *Client) formatMessage(chatID int64, messageID int64, preview bool, mess return "" } - return c.formatMessageContent(preview, c.messageToStub(message, preview, "")) + return c.formatMessageContent(preview, c.messageToStub(message, preview, ""), sender) } -func (c *Client) formatMessageContent(preview bool, message *messageStub) string { +func (c *Client) formatMessageContent(preview bool, message *messageStub, sender bool) string { var str strings.Builder // add messageid and sender if message.MessageId != 0 { str.WriteString(fmt.Sprintf("%v | ", message.MessageId)) } - str.WriteString(fmt.Sprintf("%s | ", message.Sender)) + if sender { + str.WriteString(fmt.Sprintf("%s | ", message.Sender)) + } // add date if !preview { str.WriteString( @@ -1354,7 +1360,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 return "kicked " + c.FormatContact(deleteMember.UserId) case client.TypeMessagePinMessage: pinMessage, _ := content.(*client.MessagePinMessage) - return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, nil) + return "pinned message: " + c.formatMessage(chatId, pinMessage.MessageId, preview, true, nil) case client.TypeMessageChatChangeTitle: changeTitle, _ := content.(*client.MessageChatChangeTitle) return "chat title set to: " + changeTitle.Title @@ -1648,7 +1654,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) } - replyLine := "reply: " + c.formatMessageContent(preview, tgReply) + replyLine := "reply: " + c.formatMessageContent(preview, tgReply, true) prefix = append(prefix, replyLine) replyEnd = replyStart + utf8.RuneCountInString(replyLine) @@ -1750,7 +1756,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId) var ok bool - ok, groupChatTos = c.getMUCJoinedJIDs(chatId) + ok, groupChatTos = c.getMUCJoinedJIDs(chatId, nil, true) if !ok { safeToSend = false } @@ -2604,9 +2610,20 @@ func (c *Client) TgMemberToMUCOccupant(member *client.ChatMember, chat *client.C func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, plain bool, toJid string) { sChatId := gateway.CHATNODE(chatID) - var mucJid string - if toJid != "" { - mucJid = gateway.MUCJID(chatID) + mucJid := gateway.MUCJID(chatID) + var plainTos []string + + var isMUC bool + if plain { + chat, err := c.GetChatByID(chatID, nil) + if err == nil { + isMUC = c.Session.MUC && c.IsGroup(chat) + } + } + if isMUC { + _, plainTos = c.getMUCJoinedJIDs(chatID, nil, true) + } else { + plainTos = []string{c.jid} } for i := len(messages) - 1; i >= 0; i-- { @@ -2614,23 +2631,37 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p if plain { reply, _ := c.getMessageReply(message, false, true) - sId := strconv.FormatInt(message.Id, 10) - gateway.SendMessage( - c.jid, - sChatId, - c.formatMessage(0, 0, false, message), - sId, - c.xmpp, - reply, - 0, - "", - false, - false, - false, - "", - "", - ) + + var originalFrom string + var from string + if isMUC { + senderId := c.getMessageSenderId(message) + if senderId != 0 { + originalFrom = gateway.CHATJID(senderId, true) + } + from = mucJid + "/" + c.GetMUCNickname(senderId) + } else { + from = sChatId + } + + for _, to := range plainTos { + gateway.SendMessage( + to, + from, + c.formatMessage(0, 0, false, !isMUC, message), + sId, + c.xmpp, + reply, + 0, + "", + false, + isMUC, + false, + originalFrom, + "", + ) + } } else { msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatID, message.Id) c.SendMessageToGateway( @@ -2764,6 +2795,7 @@ func (c *Client) leaveChat(chatID int64) error { return c.unsubscribe(chatID) } +// achtung: assuming a locked mucState context func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, mucState *MUCState) error { var myJid string if c.me != nil { @@ -2778,12 +2810,7 @@ func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, gateway.SPMUCStatusCodes(statusCodes), } if destroy { - var toResources []string - if mucState != nil { - for resource := range mucState.Resources { - toResources = append(toResources, resource) - } - } + _, toResources := c.getMUCJoinedJIDs(chatID, mucState, false) args = append( args, gateway.SPMUCDestroy(""), From f5a6cbf199f591cc0e5cf0f3e6f3482362a39139 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 5 Jul 2025 13:57:27 -0400 Subject: [PATCH 167/228] Fix ID-less occupant nickname when missing from group members list --- telegram/utils.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index c5d7274..b133676 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -661,14 +661,6 @@ func (c *Client) DestroyMUC(chatId int64) error { return nil } -func (c *Client) getFullName(user *client.User) string { - fullName := user.FirstName - if user.LastName != "" { - fullName = fullName + " " + user.LastName - } - return fullName -} - func (c *Client) sendMUCStatuses(chatID int64) { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() @@ -691,7 +683,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] myNickname := "me" var myJid string if c.me != nil { - myNickname = c.getFullName(c.me) + myNickname = c.GetMUCNickname(c.me.Id) myJid = gateway.CHATJID(c.me.Id, true) } myAffiliation := "member" From 19d602a99babaf451c5bff596caff8537b052918 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 6 Jul 2025 14:43:03 -0400 Subject: [PATCH 168/228] Assure all MUC presences are sent to a full JID --- telegram/handlers.go | 2 ++ telegram/utils.go | 24 ++++++++++++++++++------ xmpp/gateway/gateway.go | 10 ++++------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 3369496..f66b1cb 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -519,6 +519,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) { mucState, ok := c.mucCache[update.ChatId] if ok && mucState != nil { + _, toJids := c.getMUCJoinedJIDs(update.ChatId, mucState, false) for memberID, occupant := range mucState.Occupants { affiliation, role := c.memberStatusToAffiliationAndRole(occupant.Status, chat) if affiliation != occupant.Affiliation || role != occupant.Role { @@ -532,6 +533,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) { gateway.SPMUCJid(gateway.CHATJID(memberID, true)), gateway.SPMUCAffiliation(affiliation), gateway.SPMUCRole(role), + gateway.SPToJids(toJids), ) } } diff --git a/telegram/utils.go b/telegram/utils.go index b133676..1d9dea9 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -567,6 +567,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o for mucId, state := range c.mucCache { occupant, ok := state.Occupants[chatID] if ok { + _, toJids := c.getMUCJoinedJIDs(mucId, state, false) newMucArgs := append( newArgs, gateway.SPFrom(gateway.MUCNODE(mucId)), @@ -574,6 +575,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o gateway.SPMUCAffiliation(occupant.Affiliation), gateway.SPMUCRole(occupant.Role), gateway.SPMUCJid(chatJid), + gateway.SPToJids(toJids), ) err := c.sendPresence(newMucArgs...) if err != nil { @@ -691,6 +693,8 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] chat, _, _ := c.GetContactByID(chatID, nil) + _, toJids := c.getMUCJoinedJIDs(chatID, mucState, false) + for _, member := range members { senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat) mucState.Occupants[senderId] = &MUCOccupant{ @@ -714,6 +718,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] gateway.SPMUCAffiliation(affiliation), gateway.SPMUCRole(role), gateway.SPMUCJid(gateway.CHATJID(senderId, true)), + gateway.SPToJids(toJids), ) } @@ -726,6 +731,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] gateway.SPMUCRole(myRole), gateway.SPMUCJid(myJid), gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), + gateway.SPToJids(toJids), ) } @@ -751,6 +757,8 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s nickname := c.GetMUCNickname(memberID) + _, toJids := c.getMUCJoinedJIDs(mucID, mucState, false) + err := c.sendPresence( gateway.SPFrom(gateway.MUCNODE(mucID)), gateway.SPResource(nickname), @@ -758,6 +766,7 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s gateway.SPMUCAffiliation(affiliation), gateway.SPMUCRole(role), gateway.SPMUCJid(gateway.CHATJID(memberID, true)), + gateway.SPToJids(toJids), ) if err == nil { @@ -827,6 +836,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { unavailableStatusCodes = append(unavailableStatusCodes, 110) availableStatusCodes = append(availableStatusCodes, 110) } + _, toJids := c.getMUCJoinedJIDs(mucId, state, false) c.sendPresence( gateway.SPType("unavailable"), gateway.SPFrom(sMucId), @@ -837,6 +847,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPMUCNick(newNickname), gateway.SPMUCStatusCodes(unavailableStatusCodes), gateway.SPMUCJid(realJid), + gateway.SPToJids(toJids), ) c.sendPresence( gateway.SPFrom(sMucId), @@ -846,6 +857,7 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { gateway.SPMUCRole(oldOccupant.Role), gateway.SPMUCStatusCodes(availableStatusCodes), gateway.SPMUCJid(realJid), + gateway.SPToJids(toJids), ) } } @@ -2793,6 +2805,7 @@ func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, if c.me != nil { myJid = gateway.CHATJID(c.me.Id, true) } + _, toJids := c.getMUCJoinedJIDs(chatID, mucState, false) args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), gateway.SPResource(c.GetMUCNickname(0)), @@ -2800,14 +2813,10 @@ func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, gateway.SPMUCRole("none"), gateway.SPMUCJid(myJid), gateway.SPMUCStatusCodes(statusCodes), + gateway.SPToJids(toJids), } if destroy { - _, toResources := c.getMUCJoinedJIDs(chatID, mucState, false) - args = append( - args, - gateway.SPMUCDestroy(""), - gateway.SPToResources(toResources), - ) + args = append(args, gateway.SPMUCDestroy("")) } else { args = append(args, gateway.SPType("unavailable")) } @@ -2949,12 +2958,15 @@ func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermi } func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMemberStatus, nickname string) { + _, toJids := c.getMUCJoinedJIDs(chatID, nil, true) args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), gateway.SPResource(nickname), gateway.SPImmed(true), gateway.SPMUCJid(gateway.CHATJID(userID, true)), + gateway.SPToJids(toJids), } + var statusCodes []uint16 var newAffiliation, newRole string diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index bb3d9de..15dcbc0 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -469,8 +469,8 @@ var SPMUCStatusCodes = args.New() // SPMUCDestroy is a XEP-0045 room destruction element var SPMUCDestroy = args.NewString() -// SPToResources achieves to send the presence to certain resources only -var SPToResources = args.New() +// SPToJids achieves to send the presence to certain full jids only +var SPToJids = args.New() func newPresence(bareJid string, to string, args ...args.V) stanza.Presence { var presenceFrom string @@ -589,10 +589,8 @@ func SendPresence(component *xmpp.Component, to string, args ...args.V) error { }).Info("Got presence") var tos []string - if SPToResources.IsSet(args) { - for _, toResource := range SPToResources.Get(args).([]string) { - tos = append(tos, to + "/" + toResource) - } + if SPToJids.IsSet(args) { + tos = SPToJids.Get(args).([]string) } else { tos = []string{to} } From b5129906e3fdc1c8978494e7bb845a626c7bbed4 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 6 Jul 2025 20:44:26 -0400 Subject: [PATCH 169/228] Invite to MUC when added to it --- telegram/utils.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index 1d9dea9..4504dd0 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1734,6 +1734,11 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { case client.TypeMessageChatAddMembers: addMembers, _ := message.Content.(*client.MessageChatAddMembers) for _, memberId := range addMembers.MemberUserIds { + if c.me != nil && c.me.Id == memberId { + for resource := range c.resourcesRange() { + gateway.InviteToMUC(chatId, c.jid+"/"+resource, c.xmpp) + } + } c.mucOccupantRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(memberId)) } case client.TypeMessageChatDeleteMember: From 29e66a21d76caf93edbb2f73f3ba4fc75f25b5a6 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 6 Jul 2025 23:11:54 -0400 Subject: [PATCH 170/228] Send announcement messages from a temporary MUC occupant && fix edit/delete message regression for non-MUCs --- telegram/handlers.go | 17 ++++++++++++----- telegram/utils.go | 7 ++++++- xmpp/gateway/gateway.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index f66b1cb..2e2941f 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -276,7 +276,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { if isMUC { _, jids = c.getMUCJoinedJIDs(update.ChatId, nil, true) } else { - c.getCarbonFullJids(true, ignoredResource) + jids = c.getCarbonFullJids(true, ignoredResource) } if len(jids) == 0 { log.Info("The only resource is ignored, aborting") @@ -409,12 +409,19 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { if isGroupchat { fromJid = gateway.MUCJID(update.ChatId) _, jids = c.getMUCJoinedJIDs(update.ChatId, nil, true) + var nickname string + if chat != nil { + nickname = chat.Title + } + for _, jid := range jids { + gateway.SendMUCAnnouncement(jid, fromJid, text, nickname, c.xmpp) + } } else { fromJid = gateway.CHATNODE(update.ChatId) - c.getCarbonFullJids(true, "") - } - for _, jid := range jids { - gateway.SendTextMessage(jid, fromJid, text, c.xmpp, isGroupchat) + jids = c.getCarbonFullJids(true, "") + for _, jid := range jids { + gateway.SendTextMessage(jid, fromJid, text, c.xmpp, isGroupchat) + } } } } diff --git a/telegram/utils.go b/telegram/utils.go index 4504dd0..94c767f 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2061,7 +2061,12 @@ func (c *Client) returnMessage(returnJid string, chatID int64, text string, code if code != 0 { gateway.SendErrorMessage(returnJid, gateway.MUCJID(chatID), text, code, isGroupchat, c.xmpp) } else { - gateway.SendTextMessage(returnJid, gateway.MUCJID(chatID), text, c.xmpp, isGroupchat) + var nickname string + chat, err := c.GetChatByID(chatID, nil) + if err == nil { + nickname = chat.Title + } + gateway.SendMUCAnnouncement(returnJid, gateway.MUCJID(chatID), text, nickname, c.xmpp) } } else { gateway.SendTextMessage(returnJid, gateway.CHATNODE(chatID), text, c.xmpp, isGroupchat) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 15dcbc0..076635a 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -110,6 +110,40 @@ func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupch sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "", 0) } +// SendMUCAnnouncement creates and sends a message by a temporary occupant +func SendMUCAnnouncement(to, from, body, nickname string, component *xmpp.Component) { + if nickname == "" { + nickname = "announcement" + } + + fullFrom := from + "/" + nickname + + SendPresence( + component, + to, + SPFullFrom(fullFrom), + SPMUCAffiliation("admin"), + SPMUCRole("moderator"), + SPMUCJid(from), + ) + + var id string + if uuid, err := uuid.NewRandom(); err == nil { + id = uuid.String() + } + sendMessageWrapper(to, fullFrom, body, "", "", id, component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", 0) + + SendPresence( + component, + to, + SPType("unavailable"), + SPFullFrom(fullFrom), + SPMUCAffiliation("none"), + SPMUCRole("none"), + SPMUCJid(from), + ) +} + // SendErrorMessage creates and sends an error message stanza func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0) From b959f91a60edd8d9969d712768320d54e9c3afed Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 13 Jul 2025 05:43:59 -0400 Subject: [PATCH 171/228] Transform mentions into MUC nicknames in incoming messages --- telegram/formatter/formatter.go | 108 ++++++++++++++++++++++++++++++-- telegram/handlers.go | 1 + telegram/utils.go | 21 +++++++ 3 files changed, 124 insertions(+), 6 deletions(-) diff --git a/telegram/formatter/formatter.go b/telegram/formatter/formatter.go index a8c94a0..72337bb 100644 --- a/telegram/formatter/formatter.go +++ b/telegram/formatter/formatter.go @@ -2,6 +2,7 @@ package formatter import ( "sort" + "strings" "unicode" log "github.com/sirupsen/logrus" @@ -25,9 +26,10 @@ const ( // insertion is a piece of text in given position type insertion struct { - Offset int32 - Runes []rune - Type insertionType + Offset int32 + Runes []rune + Type insertionType + Replacing bool } // insertionStack contains the sequence of insertions @@ -74,6 +76,14 @@ func (s insertionStack) NewIterator() func() *insertion { } } +func isReplacing(entity *client.TextEntity) bool { + switch entity.Type.TextEntityTypeType() { + case client.TypeTextEntityTypeMention, client.TypeTextEntityTypeMentionName: + return true + } + return false +} + // SortEntities arranges the entities in traversal-ready order func SortEntities(entities []*client.TextEntity) []*client.TextEntity { sortedEntities := make([]*client.TextEntity, len(entities)) @@ -85,6 +95,9 @@ func SortEntities(entities []*client.TextEntity) []*client.TextEntity { if entity1.Offset < entity2.Offset { return true } else if entity1.Offset == entity2.Offset { + if entity1.Length == entity2.Length { + return !isReplacing(entity1) && isReplacing(entity2) + } return entity1.Length > entity2.Length } return false @@ -177,6 +190,22 @@ func ClaspDirectives(doubledRunes []rune, entities []*client.TextEntity) []*clie return alignedEntities } +func mentionBraces(entity *client.TextEntity, nickname string) []*insertion { + return []*insertion{ + &insertion{ + Offset: entity.Offset, + Runes: []rune(nickname), + Type: insertionOpening, + Replacing: true, + }, + &insertion{ + Offset: entity.Offset + entity.Length, + Type: insertionClosing, + Replacing: true, + }, + } +} + func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) []*insertion { return []*insertion{ &insertion{ @@ -330,11 +359,37 @@ func textToDoubledRunes(text string) []rune { return doubledRunes } +// cuts a substring back from doubled runes +func cutTextFromDoubledRunes(doubledRunes []rune, offset, length int32) string { + runeSlice := doubledRunes[offset:offset+length] + var str strings.Builder + var skipNext bool + for _, cp := range runeSlice { + if skipNext { + skipNext = false + continue + } + + str.WriteRune(cp) + + if cp > bmpCeil { + skipNext = true + } + } + return str.String() +} + +type MentionRetriever interface { + GetMUCNicknameByUsername(username string) (string, error) + GetMUCNickname(id int64) string +} + // Format traverses an already sorted list of entities and wraps the text in a markup func Format( sourceText string, entities []*client.TextEntity, markupMode MarkupModeType, + mentionRetriever MentionRetriever, ) string { if len(entities) == 0 { return sourceText @@ -369,7 +424,23 @@ func Format( startStack, endStack = startStack.rebalance(endStack, entity.Offset) - insertions := entityToMarkup(entity, doubledRunes, markupMode) + var insertions []*insertion + if entity != nil && entity.Type != nil { + switch entity.Type.TextEntityTypeType() { + case client.TypeTextEntityTypeMention: + username := cutTextFromDoubledRunes(doubledRunes, entity.Offset, entity.Length) + nickname, err := mentionRetriever.GetMUCNicknameByUsername(username) + if err == nil { + insertions = mentionBraces(entity, nickname) + } + case client.TypeTextEntityTypeMentionName: + mentionName, _ := entity.Type.(*client.TextEntityTypeMentionName) + nickname := mentionRetriever.GetMUCNickname(mentionName.UserId) + insertions = mentionBraces(entity, nickname) + default: + insertions = entityToMarkup(entity, doubledRunes, markupMode) + } + } if len(insertions) > 1 { startStack = append(startStack, insertions[0:len(insertions)-1]...) } @@ -417,6 +488,7 @@ func Format( nextInsertion := startStack.NewIterator() insertion := nextInsertion() var skipNext bool + var insideReplacingEntity bool for i, cp := range doubledRunes { if skipNext { @@ -424,19 +496,43 @@ func Format( continue } + // loop through possible multiple insertions at this point for insertion != nil && int(insertion.Offset) <= i { - markupRunes = append(markupRunes, insertion.Runes...) + if !insideReplacingEntity { + markupRunes = append(markupRunes, insertion.Runes...) + } + + // if replacing entity encountered, ignore all entities inside it until it's closed + // (replacing entities are assumed to be not nested or overlapped) + if insertion.Replacing { + if insertion.Type == insertionOpening { + insideReplacingEntity = true + } else if insertion.Type == insertionClosing { + insideReplacingEntity = false + } + } + insertion = nextInsertion() } + if insideReplacingEntity { + continue + } + markupRunes = append(markupRunes, cp) // skip two UTF-16 code units (not points actually!) if needed if cp > bmpCeil { skipNext = true } } + // flush closing insertions for insertion != nil { - markupRunes = append(markupRunes, insertion.Runes...) + if !insideReplacingEntity { + markupRunes = append(markupRunes, insertion.Runes...) + } + if insertion.Replacing && insertion.Type == insertionClosing { + insideReplacingEntity = false + } insertion = nextInsertion() } diff --git a/telegram/handlers.go b/telegram/handlers.go index 2e2941f..15c1bd5 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -340,6 +340,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { textContent.Text.Text, textContent.Text.Entities, markupFunction, + c, )) var from string diff --git a/telegram/utils.go b/telegram/utils.go index 94c767f..8567ccc 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1055,6 +1055,7 @@ func (c *Client) getMessageReply(message *client.Message, preview bool, noConten replyTo.Quote.Text.Text, replyTo.Quote.Text.Entities, c.getFormatter(), + c, ) // make the whole quote fit one line text = strings.ReplaceAll(text, "\n", " ") @@ -1392,6 +1393,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 photo.Caption.Text, photo.Caption.Entities, markupMode, + c, ) } case client.TypeMessageAudio: @@ -1403,6 +1405,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 audio.Caption.Text, audio.Caption.Entities, markupMode, + c, ) } case client.TypeMessageVideo: @@ -1414,6 +1417,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 video.Caption.Text, video.Caption.Entities, markupMode, + c, ) } case client.TypeMessageDocument: @@ -1425,6 +1429,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 document.Caption.Text, document.Caption.Entities, markupMode, + c, ) } case client.TypeMessageText: @@ -1436,6 +1441,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 text.Text.Text, text.Text.Entities, markupMode, + c, ) } case client.TypeMessageVoiceNote: @@ -1447,6 +1453,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 voice.Caption.Text, voice.Caption.Entities, markupMode, + c, ) } case client.TypeMessageVideoNote: @@ -1460,6 +1467,7 @@ func (c *Client) messageContentToText(content client.MessageContent, chatId int6 animation.Caption.Text, animation.Caption.Entities, markupMode, + c, ) } case client.TypeMessageContact: @@ -2313,6 +2321,7 @@ func (c *Client) GetChatDescription(chat *client.Chat) string { fullInfo.Bio.Text, fullInfo.Bio.Entities, c.getFormatter(), + c, ) } else if fullInfo.BotInfo != nil { if fullInfo.BotInfo.ShortDescription != "" { @@ -3081,6 +3090,18 @@ func (c *Client) getChatMemberStatus(status client.ChatMemberStatus) ChatMemberS return ChatMemberStatusIllegal } +// GetMUCNicknameByUsername implement the MentionRetriever interface for message formatters +func (c *Client) GetMUCNicknameByUsername(username string) (string, error) { + chat, err := c.client.SearchPublicChat(&client.SearchPublicChatRequest{ + Username: username, + }) + if err != nil { + return "", err + } + + return c.GetMUCNickname(chat.Id), nil +} + // GetErrorCode obtains an error code from a Telegram response error func GetErrorCode(err error) (int32, bool) { responseError, ok := err.(client.ResponseError) From 24086e903fe61d781f1206b64be48a2f311ae9d1 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 13 Jul 2025 06:02:24 -0400 Subject: [PATCH 172/228] New formatter tests --- telegram/formatter/formatter_test.go | 130 ++++++++++++++++++++++----- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/telegram/formatter/formatter_test.go b/telegram/formatter/formatter_test.go index 187d486..aaaca89 100644 --- a/telegram/formatter/formatter_test.go +++ b/telegram/formatter/formatter_test.go @@ -1,13 +1,28 @@ package formatter import ( + "errors" + "strings" "testing" "github.com/zelenin/go-tdlib/client" ) +type MentionRetrieverMock struct {} + +func (m *MentionRetrieverMock) GetMUCNicknameByUsername(username string) (string, error) { + if strings.HasPrefix(username, "@") { + return username[1:], nil + } + return "", errors.New("Я@ТЫ@Я@ТЫ@Я@ТЫ@Я@ТЫ@Я@ТЫ@") +} + +func (m *MentionRetrieverMock) GetMUCNickname(id int64) (string) { + return "42" +} + func TestNoFormatting(t *testing.T) { - markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeMarkdown) + markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "abc\ndef" { t.Errorf("No formatting expected, but: %v", markup) } @@ -20,7 +35,7 @@ func TestFormattingSimple(t *testing.T) { Length: 4, Type: &client.TextEntityTypeBold{}, }, - }, MarkupModeMarkdown) + }, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "👙**🐧🐖**" { t.Errorf("Wrong simple formatting: %v", markup) } @@ -40,7 +55,7 @@ func TestFormattingAdjacent(t *testing.T) { Url: "https://narayana.im/", }, }, - }, MarkupModeMarkdown) + }, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "a👙_🐧_[🐖](https://narayana.im/)" { t.Errorf("Wrong adjacent formatting: %v", markup) } @@ -63,7 +78,7 @@ func TestFormattingAdjacentAndNested(t *testing.T) { Length: 2, Type: &client.TextEntityTypeItalic{}, }, - }, MarkupModeMarkdown) + }, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "```\n**👙**🐧\n```_🐖_" { t.Errorf("Wrong adjacent&nested formatting: %v", markup) } @@ -208,7 +223,7 @@ func TestSortEmpty(t *testing.T) { } func TestNoFormattingXEP0393(t *testing.T) { - markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeXEP0393) + markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "abc\ndef" { t.Errorf("No formatting expected, but: %v", markup) } @@ -221,7 +236,7 @@ func TestFormattingXEP0393Simple(t *testing.T) { Length: 4, Type: &client.TextEntityTypeBold{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "👙*🐧🐖*" { t.Errorf("Wrong simple formatting: %v", markup) } @@ -241,7 +256,7 @@ func TestFormattingXEP0393Adjacent(t *testing.T) { Url: "https://narayana.im/", }, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "a👙_🐧_🐖 " { t.Errorf("Wrong adjacent formatting: %v", markup) } @@ -264,7 +279,7 @@ func TestFormattingXEP0393AdjacentAndNested(t *testing.T) { Length: 2, Type: &client.TextEntityTypeItalic{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "```\n*👙*🐧\n```_🐖_" { t.Errorf("Wrong adjacent&nested formatting: %v", markup) } @@ -287,7 +302,7 @@ func TestFormattingXEP0393AdjacentItalicBoldItalic(t *testing.T) { Length: 69, Type: &client.TextEntityTypeItalic{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "_раса двуногих крысолюдей, *которую так редко замечают, что многие отрицают само их существование*_" { t.Errorf("Wrong adjacent italic/bold-italic formatting: %v", markup) } @@ -315,7 +330,7 @@ func TestFormattingXEP0393MultipleAdjacent(t *testing.T) { Length: 1, Type: &client.TextEntityTypeItalic{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "a*bcd*_e_" { t.Errorf("Wrong multiple adjacent formatting: %v", markup) } @@ -343,7 +358,7 @@ func TestFormattingXEP0393Intersecting(t *testing.T) { Length: 1, Type: &client.TextEntityTypeBold{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "a*b*_*cd*e_" { t.Errorf("Wrong intersecting formatting: %v", markup) } @@ -361,7 +376,7 @@ func TestFormattingXEP0393InlineCode(t *testing.T) { Length: 25, Type: &client.TextEntityTypePre{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "Is `Gajim` a thing?\n\n```\necho 'Hello'\necho 'world'\n```\n\nhruck(" { t.Errorf("Wrong intersecting formatting: %v", markup) } @@ -374,7 +389,7 @@ func TestFormattingMarkdownStrikethrough(t *testing.T) { Length: 3, Type: &client.TextEntityTypeStrikethrough{}, }, - }, MarkupModeMarkdown) + }, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "Everyone ~~dis~~likes cake." { t.Errorf("Wrong strikethrough formatting: %v", markup) } @@ -387,7 +402,7 @@ func TestFormattingXEP0393Strikethrough(t *testing.T) { Length: 3, Type: &client.TextEntityTypeStrikethrough{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "Everyone ~dis~likes cake." { t.Errorf("Wrong strikethrough formatting: %v", markup) } @@ -480,7 +495,7 @@ func TestNoNewlineBlockquoteXEP0393(t *testing.T) { Length: 6, Type: &client.TextEntityTypeBlockQuote{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "yes \n> it can\n i think" { t.Errorf("Wrong blockquote formatting: %v", markup) } @@ -493,7 +508,7 @@ func TestNoNewlineBlockquoteMarkdown(t *testing.T) { Length: 6, Type: &client.TextEntityTypeBlockQuote{}, }, - }, MarkupModeMarkdown) + }, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "yes \n> it can\n\n i think" { t.Errorf("Wrong blockquote formatting: %v", markup) } @@ -506,7 +521,7 @@ func TestMultilineBlockquoteXEP0393(t *testing.T) { Length: 17, Type: &client.TextEntityTypeBlockQuote{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "> hruck\n> puck\n> \n> shuck\ntext" { t.Errorf("Wrong blockquote formatting: %v", markup) } @@ -519,7 +534,7 @@ func TestMultilineBlockquoteMarkdown(t *testing.T) { Length: 17, Type: &client.TextEntityTypeBlockQuote{}, }, - }, MarkupModeMarkdown) + }, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "> hruck\npuck\n\n> shuck\n\ntext" { t.Errorf("Wrong blockquote formatting: %v", markup) } @@ -547,7 +562,7 @@ func TestMixedBlockquoteXEP0393(t *testing.T) { Length: 2, Type: &client.TextEntityTypeStrikethrough{}, }, - }, MarkupModeXEP0393) + }, MarkupModeXEP0393, &MentionRetrieverMock{}) if markup != "> *_hruck\n> p~uc~k_\n> shuck*\ntext" { t.Errorf("Wrong blockquote formatting: %v", markup) } @@ -575,8 +590,83 @@ func TestMixedBlockquoteMarkdown(t *testing.T) { Length: 2, Type: &client.TextEntityTypeStrikethrough{}, }, - }, MarkupModeMarkdown) + }, MarkupModeMarkdown, &MentionRetrieverMock{}) if markup != "> **_hruck\np~~uc~~k_\nshuck**\n\ntext" { t.Errorf("Wrong blockquote formatting: %v", markup) } } + +func TestUsernameMention(t *testing.T) { + markup := Format("a @b c", []*client.TextEntity{ + &client.TextEntity{ + Offset: 2, + Length: 2, + Type: &client.TextEntityTypeMention{}, + }, + }, MarkupModeXEP0393, &MentionRetrieverMock{}) + if markup != "a b c" { + t.Errorf("Wrong mention formatting: %v", markup) + } +} + +func TestUsernameMentionName(t *testing.T) { + markup := Format("a bb c", []*client.TextEntity{ + &client.TextEntity{ + Offset: 2, + Length: 2, + Type: &client.TextEntityTypeMentionName{UserId: 100500}, + }, + }, MarkupModeXEP0393, &MentionRetrieverMock{}) + if markup != "a 42 c" { + t.Errorf("Wrong mention name formatting: %v", markup) + } +} + +func TestUsernameMentionNested(t *testing.T) { + markup := Format("a @b c", []*client.TextEntity{ + &client.TextEntity{ + Offset: 2, + Length: 2, + Type: &client.TextEntityTypeMention{}, + }, + &client.TextEntity{ + Offset: 2, + Length: 1, + Type: &client.TextEntityTypeBold{}, + }, + }, MarkupModeXEP0393, &MentionRetrieverMock{}) + if markup != "a b c" { + t.Errorf("Wrong formatting of mention with nested entity: %v", markup) + } +} + +func TestUsernameMentionNestedEven(t *testing.T) { + markup := Format("a @b c", []*client.TextEntity{ + &client.TextEntity{ + Offset: 2, + Length: 2, + Type: &client.TextEntityTypeMention{}, + }, + &client.TextEntity{ + Offset: 2, + Length: 2, + Type: &client.TextEntityTypeBold{}, + }, + }, MarkupModeXEP0393, &MentionRetrieverMock{}) + if markup != "a *b* c" { + t.Errorf("Wrong formatting of mention with even nested entity: %v", markup) + } +} + +func TestUsernameMentionError(t *testing.T) { + markup := Format("a bb c", []*client.TextEntity{ + &client.TextEntity{ + Offset: 2, + Length: 2, + Type: &client.TextEntityTypeMention{}, + }, + }, MarkupModeXEP0393, &MentionRetrieverMock{}) + if markup != "a bb c" { + t.Errorf("Wrong formatting of erroneous mention: %v", markup) + } +} From bb333edf69cf0ce0df3bad6886b0c44509e00b24 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 13 Jul 2025 06:13:45 -0400 Subject: [PATCH 173/228] Fix other tests --- persistence/sessions_test.go | 21 +++++++++++---------- telegram/loginwizard.go | 2 +- telegram/utils_test.go | 24 ++++++++++++++++++++---- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/persistence/sessions_test.go b/persistence/sessions_test.go index a8ec171..187445b 100644 --- a/persistence/sessions_test.go +++ b/persistence/sessions_test.go @@ -53,16 +53,17 @@ func TestSessionToMap(t *testing.T) { } m := session.ToMap() sample := map[string]string{ - "timezone": "klsf", - "keeponline": "false", - "muc": "true", - "rawmessages": "true", - "asciiarrows": "false", - "oobmode": "true", - "carbons": "false", - "hideids": "false", - "receipts": "true", - "nativeedits": "false", + "timezone": "klsf", + "keeponline": "false", + "muc": "true", + "rawmessages": "true", + "asciiarrows": "false", + "oobmode": "true", + "carbons": "false", + "hideids": "false", + "receipts": "true", + "nativeedits": "false", + "ignoregroupdeletions": "false", } if !reflect.DeepEqual(m, sample) { t.Errorf("Map does not match the sample: %v", m) diff --git a/telegram/loginwizard.go b/telegram/loginwizard.go index 0e5cc9e..296e748 100644 --- a/telegram/loginwizard.go +++ b/telegram/loginwizard.go @@ -74,7 +74,7 @@ func (c *Client) wizardStageOrPrompt(stage, message string) { log.Debugf("writing wizard stage %v", stage) c.loginWizard.nextStage <- stage } else { - log.Warn("Skipping stage %v, wizard cannot keep up", stage) + log.Warnf("Skipping stage %v, wizard cannot keep up", stage) } c.loginWizard.chanBusy = true c.locks.loginWizardWriteLock.Unlock() diff --git a/telegram/utils_test.go b/telegram/utils_test.go index e89077d..3fb9dc9 100644 --- a/telegram/utils_test.go +++ b/telegram/utils_test.go @@ -68,12 +68,28 @@ func TestFormatMessageOneline(t *testing.T) { }, } - text := (&Client{}).formatMessage(0, 0, true, &message) + text := (&Client{}).formatMessage(0, 0, true, true, &message) if text != "42 | | tist" { t.Errorf("Wrong oneline message formatting: %v", text) } } +func TestFormatMessageNoSender(t *testing.T) { + message := client.Message{ + Id: 42, + Content: &client.MessageText{ + Text: &client.FormattedText{ + Text: "tist", + }, + }, + } + + text := (&Client{}).formatMessage(0, 0, true, false, &message) + if text != "42 | tist" { + t.Errorf("Wrong nosender message formatting: %v", text) + } +} + func TestFormatMessageMultiline(t *testing.T) { message := client.Message{ Id: 42, @@ -84,7 +100,7 @@ func TestFormatMessageMultiline(t *testing.T) { }, } - text := (&Client{}).formatMessage(0, 0, true, &message) + text := (&Client{}).formatMessage(0, 0, true, true, &message) if text != "42 | | tist" { t.Errorf("Wrong multiline message formatting: %v", text) } @@ -104,7 +120,7 @@ func TestFormatMessageOnelinePreview(t *testing.T) { c := &Client{ Session: &persistence.Session{}, } - text := c.formatMessage(0, 0, false, &message) + text := c.formatMessage(0, 0, false, true, &message) if text != "42 | | 10 Jan 2008 21:20:00 | tist" { t.Errorf("Wrong oneline preview message formatting: %v", text) } @@ -124,7 +140,7 @@ func TestFormatMessageMultilinePreview(t *testing.T) { c := &Client{ Session: &persistence.Session{}, } - text := c.formatMessage(0, 0, false, &message) + text := c.formatMessage(0, 0, false, true, &message) if text != "42 | | 10 Jan 2008 21:20:00 | tist\nziz" { t.Errorf("Wrong multiline preview message formatting: %v", text) } From f875e679da86fdb0b817426dcca4ff4b0e0c068c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 20 Jul 2025 14:45:29 -0400 Subject: [PATCH 174/228] Add post-login initial configuration stages to the login wizard --- persistence/sessions.go | 32 ++++++++++++ telegram/client.go | 42 +++++++++++++++- telegram/commands.go | 45 ++++++++++++++++- telegram/connect.go | 18 ++++--- telegram/loginwizard.go | 50 ++++++++++++++++--- xmpp/handlers.go | 23 +++------ xmpp/loginwizard.go | 106 +++++++++++++++++++++++++++++++++------- 7 files changed, 267 insertions(+), 49 deletions(-) diff --git a/persistence/sessions.go b/persistence/sessions.go index 27d361b..c75dea0 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -72,6 +72,25 @@ var ConfigKeys = []string{ "ignoregroupdeletions", } +var Presets = map[string][][]string{ + "modern": [][]string{ + {"asciiarrows", "false"}, + {"oobmode", "true"}, + {"carbons", "true"}, + {"hideids", "true"}, + {"receipts", "true"}, + {"nativeedits", "true"}, + }, + "classic": [][]string{ + {"asciiarrows", "true"}, + {"oobmode", "false"}, + {"carbons", "false"}, + {"hideids", "false"}, + {"receipts", "false"}, + {"nativeedits", "false"}, + }, +} + var sessionDB *SessionsYamlDB var sessionsLock sync.Mutex @@ -369,3 +388,16 @@ func toBool(s string) (bool, error) { return false, errors.New("Invalid boolean value") } + +// NormalizeProperty converts typed properties with uncertain values to certain ones +func NormalizeProperty(key, value string) string { + if PropertyType(key) == PropertyTypeBool { + if value == "0" { + value = "false" + } + if value == "1" { + value = "true" + } + } + return value +} diff --git a/telegram/client.go b/telegram/client.go index 524ade3..0ce3cc1 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -54,6 +54,44 @@ type IntPair struct { MessageId int64 } +type barrier struct { + mu sync.Mutex + open bool + releaseCh chan struct{} +} + +// Wait blocks until the barrier is released. +func (b *barrier) Wait() { + b.mu.Lock() + if !b.open { + b.open = true + b.releaseCh = make(chan struct{}) // Reinitialize the channel + } + b.mu.Unlock() + + // Wait for the barrier to be released + <-b.releaseCh +} + +// Done releases the barrier. +func (b *barrier) Done() { + b.mu.Lock() + defer b.mu.Unlock() + + if b.open { + close(b.releaseCh) // Close the channel to release waiting goroutines + b.open = false // Mark the barrier as closed + } +} + +// IsPending checks if the barrier is currently being waited +func (b *barrier) IsPending() bool { + b.mu.Lock() + defer b.mu.Unlock() + + return b.open +} + // Client stores the metadata for lazily invoked TDlib instance type Client struct { client *client.Client @@ -71,6 +109,7 @@ type Client struct { online bool loginWizard *loginWizardMetadata + loginStage LoginStage lastAuthorizationStateType string @@ -108,6 +147,7 @@ type clientLocks struct { pinOutboxLock sync.Mutex lastMsgHashesLock sync.Mutex lastMsgIdsLock sync.RWMutex + loginFinish barrier authorizerReadLock sync.Mutex authorizerWriteLock sync.Mutex @@ -117,7 +157,7 @@ type clientLocks struct { } type loginWizardMetadata struct { - nextStage chan string + nextStage chan LoginStage chanBusy bool commandSent bool } diff --git a/telegram/commands.go b/telegram/commands.go index ad4e066..a174009 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -67,6 +67,9 @@ var transportCommands = map[string]command{ "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}, + "preset": command{1, []string{"modern|classic|pass"}, "apply a config preset", false, nil}, + "pass": command{0, []string{}, "proceed to next login stage", false, nil}, + "finish": command{0, []string{}, "skip post-login configuration", false, nil}, } var notForGroups = []ChatType{ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} @@ -346,7 +349,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } // sign out case "logout": - if !c.Online() { + if !c.Online() && !c.locks.loginFinish.IsPending() { return notOnline, false } @@ -360,6 +363,8 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } c.Session.Login = "" + c.wizardStageOrPrompt(LoginStageCancel, "") + c.online = false // cancel auth case "cancelauth": if c.Online() { @@ -465,6 +470,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin case "false": go c.MigrateFromMUCs() } + if c.loginStage == LoginStageMUC { + c.wizardStageOrPrompt(LoginStageSuccess, "") + } } gateway.DirtySessions = true @@ -519,11 +527,46 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin return c.cmdChannel(args, cmdline) case "help": return c.helpString(CommandTypeTransport, 0), true + case "preset": + cancelNextStage := false + + defer func() { + if !cancelNextStage { + go c.promptMUC() + } + }() + switch args[0] { + case "modern", "classic": + for _, row := range persistence.Presets[args[0]] { + _, err := c.Session.Set(row[0], row[1]) + if err != nil { + return err.Error(), false + } + } + gateway.DirtySessions = true + return fmt.Sprintf("Applied preset %s", args[0]), true + default: + cancelNextStage = true + return "Invalid argument. Allowed ones are " + transportCommands["preset"].Arguments[0], false + } + case "pass": + switch c.loginStage { + case LoginStagePreset: + go c.promptMUC() + case LoginStageMUC: + c.wizardStageOrPrompt(LoginStageSuccess, "") + } + case "finish": + c.wizardStageOrPrompt(LoginStageSuccess, "") } return "", true } +func (c *Client) promptMUC() { + c.wizardStageOrPrompt(LoginStageMUC, "Enable MUCs? Telegabber still supports the legacy approach of mapping Telegram group chats to personal messages in XMPP which might be suitable for some cases like logging or reliable participation in all group chats. Use `/config muc {true|false}` or /finish") +} + // ProcessChatCommand executes a command sent in a mapped chat // 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) { diff --git a/telegram/connect.go b/telegram/connect.go index f78f964..b0a2ca0 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -129,7 +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", "") + c.wizardStageOrPrompt(LoginStageCancel, "") return errors.Wrap(err, "Couldn't initialize a Telegram client instance") } @@ -138,8 +138,6 @@ 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") @@ -147,6 +145,8 @@ func (c *Client) Connect(resource string) error { c.Session.Login = c.me.PhoneNumber } + c.locks.loginFinish.Wait() + go c.updateHandler() c.online = true c.locks.authorizationReady.Unlock() @@ -259,7 +259,7 @@ func (c *Client) interactor() { if !ok { log.Warn("Interactor is disconnected") c.locks.authorizerReadLock.Unlock() - return + break } stateType := state.AuthorizationStateType() @@ -275,12 +275,12 @@ func (c *Client) interactor() { if c.Session.Login != "" { c.authorizer.PhoneNumber <- c.Session.Login } else { - c.wizardStageOrPrompt("login", "Please, enter your Telegram login via /login 12345, or use the Login Wizard via Ad-Hoc commands") + c.wizardStageOrPrompt(LoginStageLogin, "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...") - c.wizardStageOrPrompt("code", "Please, enter authorization code via /code 12345") + c.wizardStageOrPrompt(LoginStageCode, "Please, enter authorization code via /code 12345") // stage 1b: wait for registration case client.TypeAuthorizationStateWaitRegistration: log.Warn("Waiting for full name...") @@ -288,10 +288,13 @@ func (c *Client) interactor() { // stage 2: wait for 2fa case client.TypeAuthorizationStateWaitPassword: log.Warn("Waiting for 2FA password...") - c.wizardStageOrPrompt("password", "Please, enter 2FA passphrase via /password 12345") + c.wizardStageOrPrompt(LoginStagePassword, "Please, enter 2FA passphrase via /password 12345") } c.locks.authorizerReadLock.Unlock() } + if c.loginStage != LoginStageCancel { + c.wizardStageOrPrompt(LoginStagePreset, "Do you want to use a config preset? `/preset modern` enables brand new XMPP features, `/preset classic` targets legacy clients stuck in 00s. /pass proceeds to the next stage.") + } } func (c *Client) forceClose() { @@ -322,6 +325,7 @@ func (c *Client) close() { } func (c *Client) cancelAuth() { + c.wizardStageOrPrompt(LoginStageCancel, "") c.StopLoginWizard() c.close() c.Session.Login = "" diff --git a/telegram/loginwizard.go b/telegram/loginwizard.go index 296e748..3062f8c 100644 --- a/telegram/loginwizard.go +++ b/telegram/loginwizard.go @@ -7,11 +7,32 @@ import ( "github.com/zelenin/go-tdlib/client" ) +type LoginStage string +const ( + LoginStageNone LoginStage = "" + LoginStageLogin LoginStage = "login" + LoginStageCode LoginStage = "code" + LoginStagePassword LoginStage = "password" + LoginStagePreset LoginStage = "preset" + LoginStageMUC LoginStage = "muc" + LoginStageSuccess LoginStage = "success" + LoginStageCancel LoginStage = "cancel" +) + +// setLoginStage updates loginState and triggers session initialization on login sucess +func (c *Client) setLoginStage(stage LoginStage) { + c.loginStage = stage + switch c.loginStage { + case LoginStageSuccess, LoginStageCancel: + c.locks.loginFinish.Done() + } +} + // StartLoginWizard initiates a loginWizard object func (c *Client) StartLoginWizard(inCommand bool) { if c.loginWizard == nil { c.loginWizard = &loginWizardMetadata{ - nextStage: make(chan string, 1), + nextStage: make(chan LoginStage, 1), commandSent: inCommand, } } else { @@ -32,7 +53,7 @@ func (c *Client) StopLoginWizard() { } // GetLoginWizardNextStage waits for the next stage from the channel -func (c *Client) GetLoginWizardNextStage() string { +func (c *Client) GetLoginWizardNextStage() LoginStage { c.locks.loginWizardReadLock.Lock() defer c.locks.loginWizardReadLock.Unlock() @@ -45,25 +66,40 @@ func (c *Client) GetLoginWizardNextStage() string { log.Debugf("yielded stage %v", nextStage) return nextStage } else { + log.Debugf("commandSent is false") + if c.lastAuthorizationStateType == client.TypeAuthorizationStateWaitPhoneNumber || c.lastAuthorizationStateType == client.TypeAuthorizationStateClosing || c.Session.Login == "" { - return "login" + return LoginStageLogin } switch c.lastAuthorizationStateType { case client.TypeAuthorizationStateWaitCode: - return "code" + return LoginStageCode case client.TypeAuthorizationStateWaitPassword: - return "password" + return LoginStagePassword + } + + switch c.loginStage { + case LoginStagePreset: + return LoginStageMUC + case LoginStageMUC: + return LoginStageNone } } } - return "" + return LoginStageNone } -func (c *Client) wizardStageOrPrompt(stage, message string) { +func (c *Client) wizardStageOrPrompt(stage LoginStage, message string) { + log.Debugf("loginStage: %v stage: %v", c.loginStage, stage) + if c.loginStage == stage { + return + } + c.locks.loginWizardWriteLock.Lock() + c.setLoginStage(stage) if c.loginWizard == nil { c.locks.loginWizardWriteLock.Unlock() if message != "" { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index dceee3a..b94dfd6 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -1454,24 +1454,15 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command var warnString, errString string for _, field := range form.Fields { if len(field.ValuesList) > 0 { - fieldValue := 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" - } + fieldValue := persistence.NormalizeProperty(field.Var, field.ValuesList[0]) + + if gateway.MessageOutgoingPermissionVersion == 0 && field.Var == "carbons" && fieldValue == "true" { + warnString = "The server did not allow to enable carbons" + continue } oldValue, err := session.Session.Get(field.Var) @@ -1524,7 +1515,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command } } else if !toOk && command.Node == "loginwizard" { var session *telegram.Client - answer.Payload, cancelSend, session = loginWizardPayload(bare, form, resource) + answer.Payload, cancelSend, session = loginWizardPayload(bare, form, resource, command.Action) log.Debugf("immediate loginwizard payload: %#v", answer.Payload) if cancelSend { @@ -1685,7 +1676,7 @@ func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command log.Debugf("form: %#v", form) } else if !toOk && command.Node == "loginwizard" { var session *telegram.Client - answer.Payload, cancelSend, session = loginWizardPayload(bare, nil, resource) + answer.Payload, cancelSend, session = loginWizardPayload(bare, nil, resource, "") log.Debugf("immediate loginwizard payload: %#v", answer.Payload) if cancelSend { diff --git a/xmpp/loginwizard.go b/xmpp/loginwizard.go index d1b161a..53b45e5 100644 --- a/xmpp/loginwizard.go +++ b/xmpp/loginwizard.go @@ -3,6 +3,7 @@ package xmpp import ( "fmt" + "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/telegram" "dev.narayana.im/narayana/telegabber/xmpp/gateway" @@ -11,7 +12,7 @@ import ( "gosrc.io/xmpp/stanza" ) -func loginWizardPayload(bare string, requestForm *stanza.Form, resource string) (payload *stanza.Command, cancelSend bool, returnSession *telegram.Client) { +func loginWizardPayload(bare string, requestForm *stanza.Form, resource string, action string) (payload *stanza.Command, cancelSend bool, returnSession *telegram.Client) { payload = &stanza.Command{ SessionId: "loginwizard", Node: "loginwizard", @@ -21,9 +22,13 @@ func loginWizardPayload(bare string, requestForm *stanza.Form, resource string) if ok { returnSession = session + var command string + if requestForm == nil { session.StartLoginWizard(false) cancelSend = true + } else if action == stanza.CommandActionComplete || action == stanza.CommandActionExecute { + command = "/finish" } else { if len(requestForm.Fields) != 1 { setCommandPayloadError(payload, "Hey, don't tinker with the form!") @@ -35,24 +40,35 @@ func loginWizardPayload(bare string, requestForm *stanza.Form, resource string) setCommandPayloadError(payload, "No value") return } + value := field.ValuesList[0] switch field.Var { - case "login", "code", "password": + case "login", "code", "password", "preset": + if field.Var == "preset" && value == "" { + command = "/pass" + } else { + command = fmt.Sprintf("/%v %v", field.Var, value) + } + case "muc": + fieldValue := persistence.NormalizeProperty(field.Var, value) + command = fmt.Sprintf("/config muc %v", fieldValue) 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 } } + + if command != "" { + session.StartLoginWizard(true) + response, success := session.ProcessTransportCommand(command, 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())) } @@ -66,33 +82,89 @@ func sendLoginWizardResponse(component *xmpp.Component, answer *stanza.IQ, sessi Node: "loginwizard", } - nextStage := "login" + nextStage := telegram.LoginStageLogin if session != nil { nextStage = session.GetLoginWizardNextStage() } log.Debugf("nextStage: %v", nextStage) - if nextStage == "cancel" { + if nextStage == telegram.LoginStageNone || nextStage == telegram.LoginStageCancel { setCommandPayloadError(payload, "Cancelled") session.StopLoginWizard() - } else if nextStage == "success" { + } else if nextStage == telegram.LoginStageSuccess { payload.Status = stanza.CommandStatusCompleted session.StopLoginWizard() } else { required := "" + var fieldType string + var finishAction bool + var options []stanza.Option + var note string + + switch nextStage { + case telegram.LoginStagePreset: + fieldType = stanza.FieldTypeListSingle + finishAction = true + options = []stanza.Option{ + stanza.Option{ + ValuesList: []string{""}, + }, + stanza.Option{ + Label: "Modern", + ValuesList: []string{"modern"}, + }, + stanza.Option{ + Label: "Classic", + ValuesList: []string{"classic"}, + }, + } + note = "Do you want to use a config preset?\nModern enables brand new XMPP features,\nClassic targets legacy clients stuck in 00s." + case telegram.LoginStageMUC: + fieldType = stanza.FieldTypeBool + finishAction = true + + value, err := session.Session.Get("muc") + if err != nil { + log.Error("Achtung! Programming error in retrieving MUC config option") + value = "false" + } + + options = append(options, stanza.Option{ + ValuesList: []string{value}, + }) + note = "Enable MUCs? Telegabber still supports the legacy group-to-PM mapping too." + } + form := stanza.Form{ Type: stanza.FormTypeForm, Title: "Login Wizard", Fields: []*stanza.Field{ &stanza.Field{ - Var: nextStage, - Label: nextStage, + Var: string(nextStage), + Label: string(nextStage), Required: &required, + Type: fieldType, + Options: options, }, }, } payload.Status = stanza.CommandStatusExecuting payload.CommandElements = append(payload.CommandElements, &form) + + actions := stanza.Actions{ + Next: &struct{}{}, + } + if finishAction { + actions.Complete = &struct{}{} + } + payload.CommandElements = append(payload.CommandElements, &actions) + + if note != "" { + payload.CommandElements = append(payload.CommandElements, &stanza.Note{ + Text: note, + Type: stanza.CommandNoteTypeInfo, + }) + } } answer.Payload = payload From 9ee13bf582666cb04d06ed6a417a519535a2a49f Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 20 Jul 2025 14:46:58 -0400 Subject: [PATCH 175/228] Send service messages to connected full JIDs rather than one bare JID wherever possible --- telegram/handlers.go | 4 ++-- telegram/loginwizard.go | 4 +++- telegram/utils.go | 16 ++++++++++------ xmpp/handlers.go | 6 ++++-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 15c1bd5..fe9da04 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -276,7 +276,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { if isMUC { _, jids = c.getMUCJoinedJIDs(update.ChatId, nil, true) } else { - jids = c.getCarbonFullJids(true, ignoredResource) + jids = c.GetCarbonFullJids(true, ignoredResource, true) } if len(jids) == 0 { log.Info("The only resource is ignored, aborting") @@ -419,7 +419,7 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { } } else { fromJid = gateway.CHATNODE(update.ChatId) - jids = c.getCarbonFullJids(true, "") + jids = c.GetCarbonFullJids(true, "", false) for _, jid := range jids { gateway.SendTextMessage(jid, fromJid, text, c.xmpp, isGroupchat) } diff --git a/telegram/loginwizard.go b/telegram/loginwizard.go index 3062f8c..695507e 100644 --- a/telegram/loginwizard.go +++ b/telegram/loginwizard.go @@ -103,7 +103,9 @@ func (c *Client) wizardStageOrPrompt(stage LoginStage, message string) { if c.loginWizard == nil { c.locks.loginWizardWriteLock.Unlock() if message != "" { - gateway.SendServiceMessage(c.jid, message, c.xmpp) + for _, jid := range c.GetCarbonFullJids(true, "", false) { + gateway.SendServiceMessage(jid, message, c.xmpp) + } } } else { if !c.loginWizard.chanBusy { diff --git a/telegram/utils.go b/telegram/utils.go index 8567ccc..525bcf9 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1743,8 +1743,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { addMembers, _ := message.Content.(*client.MessageChatAddMembers) for _, memberId := range addMembers.MemberUserIds { if c.me != nil && c.me.Id == memberId { - for resource := range c.resourcesRange() { - gateway.InviteToMUC(chatId, c.jid+"/"+resource, c.xmpp) + for _, jid := range c.GetCarbonFullJids(true, "", false) { + gateway.InviteToMUC(chatId, jid, c.xmpp) } } c.mucOccupantRolePresence(chatId, memberId, ChatMemberStatusUnmuted, c.GetMUCNickname(memberId)) @@ -1753,8 +1753,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) c.mucOccupantRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate: - for resource := range c.resourcesRange() { - gateway.InviteToMUC(chatId, c.jid+"/"+resource, c.xmpp) + for _, jid := range c.GetCarbonFullJids(true, "", false) { + gateway.InviteToMUC(chatId, jid, c.xmpp) } } @@ -1795,7 +1795,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id var originalFrom string if len(groupChatTos) == 0 { isCarbon = c.isCarbonsEnabled() && message.IsOutgoing - jids = c.getCarbonFullJids(isCarbon, "") + jids = c.GetCarbonFullJids(isCarbon, "", true) } else { isGroupchat = true jids = groupChatTos @@ -2555,7 +2555,8 @@ func (c *Client) getFromOutbox(xmppId string) string { return resource } -func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []string { +// GetCarbonFullJids builds a set of full jids or of one bare jid for outgoing stanzas +func (c *Client) GetCarbonFullJids(isOutgoing bool, ignoredResource string, forceFull bool) []string { var jids []string if isOutgoing { for resource := range c.resourcesRange() { @@ -2563,6 +2564,9 @@ func (c *Client) getCarbonFullJids(isOutgoing bool, ignoredResource string) []st jids = append(jids, c.jid+"/"+resource) } } + if len(jids) == 0 && !forceFull { + jids = []string{c.jid} + } } else { jids = []string{c.jid} } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index b94dfd6..647f0b1 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -402,9 +402,11 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { if msg.XMLName.Space == "jabber:component:accept" && msg.Error.Code == 401 { suffix := "@" + msg.From - for bare := range sessions { + for bare, session := range sessions { if strings.HasSuffix(bare, suffix) { - gateway.SendServiceMessage(bare, "Your server \""+msg.From+"\" does not allow to send carbons", component) + for _, jid := range session.GetCarbonFullJids(true, "", false) { + gateway.SendServiceMessage(jid, "Your server \""+msg.From+"\" does not allow to send carbons", component) + } } } } From 2dd521c3ed5b0d494d9b9f6414bbdf91045e9f07 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 21 Jul 2025 12:29:07 -0400 Subject: [PATCH 176/228] Avoid post-login configuration on reconnects and relogins during a session --- telegram/client.go | 9 +++++++++ telegram/connect.go | 10 +++++++++- telegram/loginwizard.go | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/telegram/client.go b/telegram/client.go index 0ce3cc1..445281a 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -57,12 +57,19 @@ type IntPair struct { type barrier struct { mu sync.Mutex open bool + released bool releaseCh chan struct{} } // Wait blocks until the barrier is released. func (b *barrier) Wait() { b.mu.Lock() + + if b.released { + b.mu.Unlock() + return + } + if !b.open { b.open = true b.releaseCh = make(chan struct{}) // Reinitialize the channel @@ -81,6 +88,8 @@ func (b *barrier) Done() { if b.open { close(b.releaseCh) // Close the channel to release waiting goroutines b.open = false // Mark the barrier as closed + } else { + b.released = true } } diff --git a/telegram/connect.go b/telegram/connect.go index b0a2ca0..afdf5f3 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -145,9 +145,11 @@ func (c *Client) Connect(resource string) error { c.Session.Login = c.me.PhoneNumber } + log.Debug("waiting for loginFinish") c.locks.loginFinish.Wait() go c.updateHandler() + log.Warn("Going online") c.online = true c.locks.authorizationReady.Unlock() c.addResource(resource) @@ -163,6 +165,7 @@ func (c *Client) Connect(resource string) error { gateway.SubscribeToTransport(c.xmpp, c.jid) c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login)) }() + log.Warn("Client connected!") return nil } @@ -248,6 +251,7 @@ func (c *Client) Disconnect(resource string, quit bool) bool { } func (c *Client) interactor() { + wasSessionLoginEmpty := c.Session.Login == "" for { c.locks.authorizerReadLock.Lock() if c.authorizer == nil { @@ -293,7 +297,11 @@ func (c *Client) interactor() { c.locks.authorizerReadLock.Unlock() } if c.loginStage != LoginStageCancel { - c.wizardStageOrPrompt(LoginStagePreset, "Do you want to use a config preset? `/preset modern` enables brand new XMPP features, `/preset classic` targets legacy clients stuck in 00s. /pass proceeds to the next stage.") + if wasSessionLoginEmpty { + c.wizardStageOrPrompt(LoginStagePreset, "Do you want to use a config preset? `/preset modern` enables brand new XMPP features, `/preset classic` targets legacy clients stuck in 00s. /pass proceeds to the next stage.") + } else { + c.wizardStageOrPrompt(LoginStageSuccess, "") + } } } diff --git a/telegram/loginwizard.go b/telegram/loginwizard.go index 695507e..1eb4ac8 100644 --- a/telegram/loginwizard.go +++ b/telegram/loginwizard.go @@ -26,6 +26,7 @@ func (c *Client) setLoginStage(stage LoginStage) { case LoginStageSuccess, LoginStageCancel: c.locks.loginFinish.Done() } + log.Debugf("set loginStage %v", stage) } // StartLoginWizard initiates a loginWizard object From 10fc1d26ef2fbdbfae520bd9fb68bea6a7ab8d05 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 22 Jul 2025 13:51:55 -0400 Subject: [PATCH 177/228] Enable MUC support for supergroups --- telegram/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index 525bcf9..191a55d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2413,7 +2413,7 @@ func (c *Client) IsGroup(chat *client.Chat) bool { return false } typ := chat.Type.ChatTypeType() - return typ == client.TypeChatTypeBasicGroup + return typ == client.TypeChatTypeBasicGroup || typ == client.TypeChatTypeSupergroup } // subscribe to a Telegram ID From 422ac9dea69a3c09afc2977b7d6d00607e95465c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 22 Jul 2025 13:53:19 -0400 Subject: [PATCH 178/228] Support retrieving chat administrators/owner in supergroups --- telegram/utils.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 191a55d..2d0e0b8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2732,11 +2732,14 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member return []*client.ChatMember{chatMember}, nil } + } else if chatType == client.TypeChatTypeSupergroup { + // noop, use complex filtering strategy later + } else { + return nil, errors.New("Creator not found") } - - return nil, errors.New("Creator not found") } + var administratorsRequest bool var filters []client.ChatMembersFilter switch membersList { case MembersListMembers: @@ -2747,7 +2750,8 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}} case MembersListBannedAndAdministrators: filters = []client.ChatMembersFilter{&client.ChatMembersFilterBanned{}, &client.ChatMembersFilterAdministrators{}} - case MembersListAdministrators: + case MembersListAdministrators, MembersListCreators: + administratorsRequest = true filters = []client.ChatMembersFilter{&client.ChatMembersFilterAdministrators{}} } @@ -2792,6 +2796,18 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member } } + administrators := make(map[int64]*client.ChatAdministrator) + if administratorsRequest { + chatAdministrators, err := c.client.GetChatAdministrators(&client.GetChatAdministratorsRequest{ + ChatId: chatID, + }) + if err == nil { + for _, administrator := range chatAdministrators.Administrators { + administrators[administrator.UserId] = administrator + } + } + } + var members []*client.ChatMember for _, filter := range filters { chatMembers, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ @@ -2803,7 +2819,17 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member if err != nil { return nil, err } - members = append(members, chatMembers.Members...) + for _, member := range chatMembers.Members { + switch membersList { + case MembersListAdministrators, MembersListCreators: + senderId := c.GetSenderId(member.MemberId) + administrator, ok := administrators[senderId] + if !ok || (administrator.IsOwner == (membersList == MembersListAdministrators)) { + continue + } + } + members = append(members, member) + } } return members, nil } From a52bede7f974597c24d5f3658c4e26e227340e1c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 22 Jul 2025 14:25:29 -0400 Subject: [PATCH 179/228] Call OpenChat/CloseChat on MUC join/leave to achieve receiving supergroup updates --- telegram/utils.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index 2d0e0b8..78e6008 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -622,6 +622,10 @@ func (c *Client) JoinMUC(chatId int64, resource string, limit *MessageLimit) { } c.sendMUCSubject(chatId, resource) + + c.client.OpenChat(&client.OpenChatRequest{ + ChatId: chatId, + }) } // LeaveMUC removes MUC date from the cache @@ -639,6 +643,10 @@ func (c *Client) LeaveMUC(chatId int64, resource string) { if len(mucState.Resources) == 0 { delete(c.mucCache, chatId) } + + c.client.CloseChat(&client.CloseChatRequest{ + ChatId: chatId, + }) } // DestroyMUC removes everyone from the MUC From e2c5c78882c20d26f860ca84683f96bcc5d03728 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 22 Jul 2025 14:34:18 -0400 Subject: [PATCH 180/228] Fix crash when joining an inaccessible group --- telegram/utils.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 78e6008..3c20ec7 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -680,12 +680,14 @@ func (c *Client) sendMUCStatuses(chatID int64) { c.mucCache[chatID] = mucState } - members, _ := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ + members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ ChatId: chatID, Limit: 200, Filter: &client.ChatMembersFilterMembers{}, }) - c.updateMUCOccupants(mucState, chatID, members.Members) + if err == nil { + c.updateMUCOccupants(mucState, chatID, members.Members) + } } func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []*client.ChatMember) { From 74b2dff9f55a7377d72338f02b628703d3532690 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 22 Jul 2025 14:54:14 -0400 Subject: [PATCH 181/228] Properly yield a 403 error when banned from a supergroup --- xmpp/handlers.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 647f0b1..9e00976 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -601,8 +601,9 @@ func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresen } status := session.GetMyStatusInChat(chatId) - // TODO: seems to be impossible for basic groups, check back when supergroups are supported - if status == telegram.ChatMemberStatusBanned { + log.Debugf("status in group %v: %v", chatId, status) + switch status { + case telegram.ChatMemberStatusBanned, telegram.ChatMemberStatusIllegal: presenceReplySetError(reply, 403) return } @@ -2093,6 +2094,9 @@ func presenceReplySetError(reply *stanza.Presence, code int) { case 400: reply.Error.Type = stanza.ErrorTypeModify reply.Error.Reason = "jid-malformed" + case 403: + reply.Error.Type = stanza.ErrorTypeAuth + reply.Error.Reason = "forbidden" case 404: reply.Error.Type = stanza.ErrorTypeCancel reply.Error.Reason = "item-not-found" From a909b868319d35d7e1e9f3bd28732fd771370b19 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 23 Jul 2025 04:06:35 -0400 Subject: [PATCH 182/228] Fix infinite loop because of error presences --- xmpp/handlers.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 9e00976..52f0071 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -669,6 +669,10 @@ func tryHandleMUCPresence(s xmpp.Sender, p stanza.Presence) { return } + if p.Type == stanza.PresenceTypeError { + return + } + if !session.MUCHasResource(chatId, fromResource) { // groupchat 1.0 join gateway.SendPresence( From f3eeba7273c421156cdf71f0ae3d2b83c998166a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 23 Jul 2025 17:03:19 -0400 Subject: [PATCH 183/228] Upgrade MUC occupants map to LRU --- telegram/client.go | 21 --- telegram/handlers.go | 6 +- telegram/muc.go | 181 +++++++++++++++++++ telegram/muc_test.go | 419 +++++++++++++++++++++++++++++++++++++++++++ telegram/utils.go | 30 ++-- 5 files changed, 618 insertions(+), 39 deletions(-) create mode 100644 telegram/muc.go create mode 100644 telegram/muc_test.go diff --git a/telegram/client.go b/telegram/client.go index 445281a..52fb326 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -21,27 +21,6 @@ type DelayedStatus struct { TimestampExpired int64 } -// MUCState holds MUC metadata -type MUCState struct { - Resources map[string]bool - Occupants map[int64]*MUCOccupant -} - -// MUCOccupant represents a MUC occupant -type MUCOccupant struct { - Nickname string - Affiliation string - Role string - Status client.ChatMemberStatus -} - -func NewMUCState() *MUCState { - return &MUCState{ - Resources: make(map[string]bool), - Occupants: make(map[int64]*MUCOccupant), - } -} - // HashedAvatar stores a SHA-1 hash and a Telegram file ID type HashedAvatar struct { Hash string diff --git a/telegram/handlers.go b/telegram/handlers.go index fe9da04..0880332 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -506,7 +506,7 @@ func (c *Client) updateBasicGroupFullInfo(update *client.UpdateBasicGroupFullInf mucState, ok := c.mucCache[chatID] if ok && mucState != nil { - mucState.Occupants = make(map[int64]*MUCOccupant) + mucState.Occupants.Clear() c.updateMUCOccupants(mucState, chatID, update.BasicGroupFullInfo.Members) } @@ -528,7 +528,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) { mucState, ok := c.mucCache[update.ChatId] if ok && mucState != nil { _, toJids := c.getMUCJoinedJIDs(update.ChatId, mucState, false) - for memberID, occupant := range mucState.Occupants { + for occupant := range mucState.Occupants.Range() { affiliation, role := c.memberStatusToAffiliationAndRole(occupant.Status, chat) if affiliation != occupant.Affiliation || role != occupant.Role { occupant.Affiliation = affiliation @@ -538,7 +538,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) { gateway.SPFrom(gateway.MUCNODE(update.ChatId)), gateway.SPResource(occupant.Nickname), gateway.SPImmed(true), - gateway.SPMUCJid(gateway.CHATJID(memberID, true)), + gateway.SPMUCJid(gateway.CHATJID(occupant.key, true)), gateway.SPMUCAffiliation(affiliation), gateway.SPMUCRole(role), gateway.SPToJids(toJids), diff --git a/telegram/muc.go b/telegram/muc.go new file mode 100644 index 0000000..c933ae1 --- /dev/null +++ b/telegram/muc.go @@ -0,0 +1,181 @@ +package telegram + +import ( + "sync" + + "github.com/zelenin/go-tdlib/client" + log "github.com/sirupsen/logrus" +) + +const MUCOccupantsLimit int32 = 200 + +// MUCState holds MUC metadata +type MUCState struct { + Resources map[string]bool + Occupants *MUCOccupantsLRU +} + +// MUCOccupant represents a MUC occupant +type MUCOccupant struct { + Nickname string + Affiliation string + Role string + Status client.ChatMemberStatus + prev *MUCOccupant + next *MUCOccupant + key int64 +} + +func (o *MUCOccupant) cutOut() (prev, next *MUCOccupant) { + prev = o.prev + next = o.next + + // -- * --- * -X- * -X- * --- * -- + o.prev = nil + o.next = nil + if prev != nil { + prev.next = next + } + if next != nil { + next.prev = prev + } + + return +} + +func NewMUCState() *MUCState { + return &MUCState{ + Resources: make(map[string]bool), + Occupants: NewMUCOccupantsLRU(), + } +} + +type MUCOccupantsLRU struct { + m map[int64]*MUCOccupant + oldest *MUCOccupant + newest *MUCOccupant + lock sync.Mutex +} + +func NewMUCOccupantsLRU() *MUCOccupantsLRU { + return &MUCOccupantsLRU{ + m: make(map[int64]*MUCOccupant), + } +} + +func (lru *MUCOccupantsLRU) Get(key int64) (*MUCOccupant, bool) { + lru.lock.Lock() + defer lru.lock.Unlock() + + occupant, ok := lru.m[key] + return occupant, ok +} + +func (lru *MUCOccupantsLRU) cutOut(oldOccupant *MUCOccupant) (prev, next *MUCOccupant) { + prev, next = oldOccupant.cutOut() + if lru.oldest == oldOccupant { + lru.oldest = next + } + if lru.newest == oldOccupant { + lru.newest = prev + } + + return +} + +func (lru *MUCOccupantsLRU) insertNewest(occupant *MUCOccupant) { + lru.newest.next = occupant + occupant.prev = lru.newest + occupant.next = nil + lru.newest = occupant +} + +func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) { + lru.lock.Lock() + defer lru.lock.Unlock() + + occupant.key = key + + oldOccupant, oldOk := lru.m[key] + lru.m[key] = occupant + + if oldOk { + lru.cutOut(oldOccupant) + } + + if (lru.oldest == nil) != (lru.newest == nil) { + log.Fatal("MRD MUDAQ") + } + + if lru.oldest == nil && lru.newest == nil { + lru.oldest = occupant + lru.newest = occupant + occupant.prev = nil + occupant.next = nil + } else { + lru.insertNewest(occupant) + } + + if len(lru.m) > int(MUCOccupantsLimit) && lru.oldest != nil { + delete(lru.m, lru.oldest.key) + lru.cutOut(lru.oldest) + } +} + +func (lru *MUCOccupantsLRU) Delete(key int64) { + lru.lock.Lock() + defer lru.lock.Unlock() + + oldOccupant, oldOk := lru.m[key] + delete(lru.m, key) + + if oldOk { + lru.cutOut(oldOccupant) + } +} + +func (lru *MUCOccupantsLRU) Bump(occupant *MUCOccupant) { + lru.lock.Lock() + defer lru.lock.Unlock() + + if lru.newest == occupant { + // already at the top, nothing to do + return + } + + lru.cutOut(occupant) + lru.insertNewest(occupant) +} + +func (lru *MUCOccupantsLRU) Range() chan *MUCOccupant { + lru.lock.Lock() + + occupantChan := make(chan *MUCOccupant, 1) + + go func() { + defer func() { + lru.lock.Unlock() + close(occupantChan) + }() + + for _, occupant := range lru.m { + occupantChan <- occupant + } + }() + + return occupantChan +} + +func (lru *MUCOccupantsLRU) Clear() { + lru.lock.Lock() + defer lru.lock.Unlock() + + for _, occupant := range lru.m { + occupant.prev = nil + occupant.next = nil + } + lru.m = make(map[int64]*MUCOccupant) + + lru.oldest = nil + lru.newest = nil +} diff --git a/telegram/muc_test.go b/telegram/muc_test.go new file mode 100644 index 0000000..f03fef1 --- /dev/null +++ b/telegram/muc_test.go @@ -0,0 +1,419 @@ +package telegram + +import ( + "testing" +) + +// -x->[]-x-> +(.) +func TestSetMUCOccupantsLRUSetInitiallyEmpty(t *testing.T) { + // init + occupants := NewMUCOccupantsLRU() + + // addition + newOccupant := &MUCOccupant{} + occupants.Set(1, newOccupant) + + // checks + if occupants.oldest != newOccupant || newOccupant.prev != nil || newOccupant.next != nil || occupants.newest != newOccupant { + t.Error("Broken") + } +} + +func testMUCOccupantsLRUChainOfOne() (occupants *MUCOccupantsLRU, occupant1 *MUCOccupant) { + occupants = NewMUCOccupantsLRU() + occupant1 = &MUCOccupant{} + occupants.m[1] = occupant1 + occupants.oldest = occupant1 + occupants.newest = occupant1 + + return +} + +func testMUCOccupantsLRUChainOfThree() (occupants *MUCOccupantsLRU, occupant1, occupant2, occupant3 *MUCOccupant) { + occupants = NewMUCOccupantsLRU() + occupant1 = &MUCOccupant{} + occupant2 = &MUCOccupant{} + occupant3 = &MUCOccupant{} + occupants.m[1] = occupant1 + occupants.m[2] = occupant2 + occupants.m[3] = occupant3 + occupants.oldest = occupant1 + occupants.newest = occupant3 + occupant1.next = occupant2 + occupant2.prev = occupant1 + occupant2.next = occupant3 + occupant3.prev = occupant2 + + return +} + +// ->[]->()-> +(.) +func TestSetMUCOccupantsLRUSetOneOther(t *testing.T) { + // init + occupants, occupant1 := testMUCOccupantsLRUChainOfOne() + + // addition + newOccupant := &MUCOccupant{} + occupants.Set(2, newOccupant) + + // checks + if occupants.oldest != occupant1 || occupant1.prev != nil || occupant1.next != newOccupant || newOccupant.prev != occupant1 || newOccupant.next != nil || occupants.newest != newOccupant { + t.Error("Broken") + } +} + +// ->[]->()->()->()-> +(.) +func TestSetMUCOccupantsLRUSetThreeOthers(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // addition + newOccupant := &MUCOccupant{} + occupants.Set(4, newOccupant) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant2 || + occupant2.prev != occupant1 || + occupant2.next != occupant3 || + occupant3.prev != occupant2 || + occupant3.next != newOccupant || + newOccupant.prev != occupant3 || + newOccupant.next != nil || + occupants.newest != newOccupant { + t.Error("Broken") + } +} + +// ->[]->(.)->()->()-> +(.) +func TestSetMUCOccupantsLRUSetReplaceFirst(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // addition + newOccupant := &MUCOccupant{} + occupants.Set(1, newOccupant) + + // checks + if occupants.oldest != occupant2 || + occupant2.prev != nil || + occupant2.next != occupant3 || + occupant3.prev != occupant2 || + occupant3.next != newOccupant || + newOccupant.prev != occupant3 || + newOccupant.next != nil || + occupants.newest != newOccupant || + occupant1.prev != nil || + occupant1.next != nil { + t.Error("Broken") + } +} + +// ->[]->()->(.)->()-> +(.) +func TestSetMUCOccupantsLRUSetReplaceMiddle(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // addition + newOccupant := &MUCOccupant{} + occupants.Set(2, newOccupant) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant3 || + occupant3.prev != occupant1 || + occupant3.next != newOccupant || + newOccupant.prev != occupant3 || + newOccupant.next != nil || + occupants.newest != newOccupant || + occupant2.prev != nil || + occupant2.next != nil { + t.Error("Broken") + } +} + +// ->[]->()->()->(.)-> +(.) +func TestSetMUCOccupantsLRUSetReplaceLast(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // addition + newOccupant := &MUCOccupant{} + occupants.Set(3, newOccupant) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant2 || + occupant2.prev != occupant1 || + occupant2.next != newOccupant || + newOccupant.prev != occupant2 || + newOccupant.next != nil || + occupants.newest != newOccupant || + occupant3.prev != nil || + occupant3.next != nil { + t.Error("Broken") + } +} + +// ->[]->(.)-> +(.) +func TestSetMUCOccupantsLRUSetReplaceOnly(t *testing.T) { + // init + occupants, occupant1 := testMUCOccupantsLRUChainOfOne() + + // addition + newOccupant := &MUCOccupant{} + occupants.Set(1, newOccupant) + + // checks + if occupants.oldest != newOccupant || + occupants.newest != newOccupant || + newOccupant.prev != nil || + newOccupant.next != nil || + occupant1.prev != nil || + occupant1.next != nil { + t.Error("Broken") + } +} + +// ->[]->(.)->()->()-> +s(.) +func TestSetMUCOccupantsLRUSetReplaceFirstWithSame(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // addition + occupants.Set(1, occupant1) + + // checks + if occupants.oldest != occupant2 || + occupant2.prev != nil || + occupant2.next != occupant3 || + occupant3.prev != occupant2 || + occupant3.next != occupant1 || + occupant1.prev != occupant3 || + occupant1.next != nil || + occupants.newest != occupant1 { + t.Error("Broken") + } +} + +// ->[]->()->(.)->()-> +s(.) +func TestSetMUCOccupantsLRUSetReplaceMiddleWithSame(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // addition + occupants.Set(2, occupant2) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant3 || + occupant3.prev != occupant1 || + occupant3.next != occupant2 || + occupant2.prev != occupant3 || + occupant2.next != nil || + occupants.newest != occupant2 { + t.Error("Broken") + } +} + +// ->[]->()->()->(.)-> +s(.) +func TestSetMUCOccupantsLRUSetReplaceLastWithSame(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // addition + occupants.Set(3, occupant3) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant2 || + occupant2.prev != occupant1 || + occupant2.next != occupant3 || + occupant3.prev != occupant2 || + occupant3.next != nil || + occupants.newest != occupant3 { + t.Error("Broken") + } +} + +// ->[]->(.)-> +(.) +func TestSetMUCOccupantsLRUSetReplaceOnlyWithSame(t *testing.T) { + // init + occupants, occupant1 := testMUCOccupantsLRUChainOfOne() + + // addition + occupants.Set(1, occupant1) + + // checks + if occupants.oldest != occupant1 || + occupants.newest != occupant1 || + occupant1.prev != nil || + occupant1.next != nil { + t.Error("Broken") + } +} + +// ->[]->(X)-> +func TestSetMUCOccupantsLRUDeleteOnly(t *testing.T) { + // init + occupants, occupant1 := testMUCOccupantsLRUChainOfOne() + + // deletion + occupants.Delete(1) + + // checks + if occupants.oldest != nil || + occupants.newest != nil || + occupant1.prev != nil || + occupant1.next != nil { + t.Error("Broken") + } +} + +// ->[]->(X)->()->()-> +func TestSetMUCOccupantsLRUDeleteFirst(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // deletion + occupants.Delete(1) + + // checks + if occupants.oldest != occupant2 || + occupant2.prev != nil || + occupant2.next != occupant3 || + occupant3.prev != occupant2 || + occupant3.next != nil || + occupants.newest != occupant3 || + occupant1.prev != nil || + occupant1.next != nil { + t.Error("Broken") + } +} + +// ->[]->()->(X)->()-> +func TestSetMUCOccupantsLRUDeleteMiddle(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // deletion + occupants.Delete(2) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant3 || + occupant3.prev != occupant1 || + occupant3.next != nil || + occupants.newest != occupant3 || + occupant2.prev != nil || + occupant2.next != nil { + t.Error("Broken") + } +} + +// ->[]->()->()->(X)-> +func TestSetMUCOccupantsLRUDeleteLast(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // deletion + occupants.Delete(3) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant2 || + occupant2.prev != occupant1 || + occupant2.next != nil || + occupants.newest != occupant2 || + occupant3.prev != nil || + occupant3.next != nil { + t.Error("Broken") + } +} + +// ->[]->(.)-> +func TestSetMUCOccupantsLRUBumpOnly(t *testing.T) { + // init + occupants, occupant1 := testMUCOccupantsLRUChainOfOne() + + // bump + occupants.Bump(occupant1) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != nil || + occupants.newest != occupant1 { + t.Error("Broken") + } +} + +// ->[]->(.)->()->()-> +func TestSetMUCOccupantsLRUBumpFirst(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // bump + occupants.Bump(occupant1) + + // checks + if occupants.oldest != occupant2 || + occupant2.prev != nil || + occupant2.next != occupant3 || + occupant3.prev != occupant2 || + occupant3.next != occupant1 || + occupant1.prev != occupant3 || + occupant1.next != nil || + occupants.newest != occupant1 { + t.Error("Broken") + } +} + +// ->[]->()->(.)->()-> +func TestSetMUCOccupantsLRUBumpMiddle(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // bump + occupants.Bump(occupant2) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant3 || + occupant3.prev != occupant1 || + occupant3.next != occupant2 || + occupant2.prev != occupant3 || + occupant2.next != nil || + occupants.newest != occupant2 { + t.Error("Broken") + } +} + +// ->[]->()->()->(.)-> +func TestSetMUCOccupantsLRUBumpLast(t *testing.T) { + // init + occupants, occupant1, occupant2, occupant3 := testMUCOccupantsLRUChainOfThree() + + // bump + occupants.Bump(occupant3) + + // checks + if occupants.oldest != occupant1 || + occupant1.prev != nil || + occupant1.next != occupant2 || + occupant2.prev != occupant1 || + occupant2.next != occupant3 || + occupant3.prev != occupant2 || + occupant3.next != nil || + occupants.newest != occupant3 { + t.Error("Broken") + } +} diff --git a/telegram/utils.go b/telegram/utils.go index 3c20ec7..aadcc28 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -565,7 +565,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o c.locks.mucCacheLock.Lock() chatJid := gateway.CHATJID(chatID, true) for mucId, state := range c.mucCache { - occupant, ok := state.Occupants[chatID] + occupant, ok := state.Occupants.Get(chatID) if ok { _, toJids := c.getMUCJoinedJIDs(mucId, state, false) newMucArgs := append( @@ -682,7 +682,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { members, err := c.client.SearchChatMembers(&client.SearchChatMembersRequest{ ChatId: chatID, - Limit: 200, + Limit: MUCOccupantsLimit, Filter: &client.ChatMembersFilterMembers{}, }) if err == nil { @@ -707,12 +707,12 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] for _, member := range members { senderId, nickname, affiliation, role := c.TgMemberToMUCOccupant(member, chat) - mucState.Occupants[senderId] = &MUCOccupant{ + mucState.Occupants.Set(senderId, &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, Status: member.Status, - } + }) if c.me != nil && senderId == c.me.Id { myNickname = nickname @@ -753,7 +753,7 @@ func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool { return false // no MUC to be added to } - _, ok = mucState.Occupants[memberID] + _, ok = mucState.Occupants.Get(memberID) return ok } @@ -780,12 +780,12 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s ) if err == nil { - mucState.Occupants[memberID] = &MUCOccupant{ + mucState.Occupants.Set(memberID, &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, Status: status, - } + }) return true } @@ -830,14 +830,14 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { realJid := gateway.CHATJID(memberID, true) for mucId, state := range c.mucCache { - oldOccupant, ok := state.Occupants[memberID] + oldOccupant, ok := state.Occupants.Get(memberID) if ok { - state.Occupants[memberID] = &MUCOccupant{ + state.Occupants.Set(memberID, &MUCOccupant{ Nickname: newNickname, Affiliation: oldOccupant.Affiliation, Role: oldOccupant.Role, Status: oldOccupant.Status, - } + }) sMucId := gateway.MUCNODE(mucId) unavailableStatusCodes := []uint16{303, 210} @@ -921,7 +921,7 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { if !ok || mucState == nil { return "", false } - occupant, ok := mucState.Occupants[c.me.Id] + occupant, ok := mucState.Occupants.Get(c.me.Id) if !ok { return "", false } @@ -938,9 +938,9 @@ func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { return 0 } - for memberId, occupant := range mucState.Occupants { + for occupant := range mucState.Occupants.Range() { if occupant.Nickname == nickname { - return memberId + return occupant.key } } @@ -3068,9 +3068,9 @@ func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMember mucState, ok := c.mucCache[chatID] if ok && mucState != nil { if status == ChatMemberStatusKicked || status == ChatMemberStatusBanned { - delete(mucState.Occupants, userID) + mucState.Occupants.Delete(userID) } else { - occupant, ok := mucState.Occupants[userID] + occupant, ok := mucState.Occupants.Get(userID) if ok { occupant.Affiliation = newAffiliation occupant.Role = newRole From f9be86b5dbcc7523dbde930cd3d006d5a1024425 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 24 Jul 2025 12:34:05 -0400 Subject: [PATCH 184/228] Bump MUC occupants and send unavailable presences for ones removed from LRU --- telegram/muc.go | 10 +++++++++- telegram/utils.go | 42 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/telegram/muc.go b/telegram/muc.go index c933ae1..9021de5 100644 --- a/telegram/muc.go +++ b/telegram/muc.go @@ -90,7 +90,8 @@ func (lru *MUCOccupantsLRU) insertNewest(occupant *MUCOccupant) { lru.newest = occupant } -func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) { +// Set adds or replaces an occupant and possibly returns an occupant removed instead because of overflow +func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) (deleted *MUCOccupant) { lru.lock.Lock() defer lru.lock.Unlock() @@ -117,11 +118,15 @@ func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) { } if len(lru.m) > int(MUCOccupantsLimit) && lru.oldest != nil { + deleted = lru.oldest delete(lru.m, lru.oldest.key) lru.cutOut(lru.oldest) } + + return } +// Delete occupant by member ID func (lru *MUCOccupantsLRU) Delete(key int64) { lru.lock.Lock() defer lru.lock.Unlock() @@ -134,6 +139,7 @@ func (lru *MUCOccupantsLRU) Delete(key int64) { } } +// Bump raises the occupant in LRU func (lru *MUCOccupantsLRU) Bump(occupant *MUCOccupant) { lru.lock.Lock() defer lru.lock.Unlock() @@ -147,6 +153,7 @@ func (lru *MUCOccupantsLRU) Bump(occupant *MUCOccupant) { lru.insertNewest(occupant) } +// Range loops over all occupants func (lru *MUCOccupantsLRU) Range() chan *MUCOccupant { lru.lock.Lock() @@ -166,6 +173,7 @@ func (lru *MUCOccupantsLRU) Range() chan *MUCOccupant { return occupantChan } +// Clear properly removes all occupants and their possible mutual references (not necessary in Golang, yet still) func (lru *MUCOccupantsLRU) Clear() { lru.lock.Lock() defer lru.lock.Unlock() diff --git a/telegram/utils.go b/telegram/utils.go index aadcc28..937ca8b 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -567,6 +567,11 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o for mucId, state := range c.mucCache { occupant, ok := state.Occupants.Get(chatID) if ok { + if show == "" { + // Bump occupants who just went online + state.Occupants.Bump(occupant) + } + _, toJids := c.getMUCJoinedJIDs(mucId, state, false) newMucArgs := append( newArgs, @@ -753,7 +758,8 @@ func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool { return false // no MUC to be added to } - _, ok = mucState.Occupants.Get(memberID) + occupant, ok := mucState.Occupants.Get(memberID) + mucState.Occupants.Bump(occupant) return ok } @@ -780,12 +786,14 @@ func (c *Client) addMUCOccupant(mucID int64, memberID int64, affiliation, role s ) if err == nil { - mucState.Occupants.Set(memberID, &MUCOccupant{ + deleted := mucState.Occupants.Set(memberID, &MUCOccupant{ Nickname: nickname, Affiliation: affiliation, Role: role, Status: status, }) + c.kickStaleOccupant(mucID, deleted, mucState) + return true } @@ -832,12 +840,13 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { for mucId, state := range c.mucCache { oldOccupant, ok := state.Occupants.Get(memberID) if ok { - state.Occupants.Set(memberID, &MUCOccupant{ + deleted := state.Occupants.Set(memberID, &MUCOccupant{ Nickname: newNickname, Affiliation: oldOccupant.Affiliation, Role: oldOccupant.Role, Status: oldOccupant.Status, }) + c.kickStaleOccupant(mucId, deleted, state) sMucId := gateway.MUCNODE(mucId) unavailableStatusCodes := []uint16{303, 210} @@ -925,6 +934,7 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { if !ok { return "", false } + mucState.Occupants.Bump(occupant) return occupant.Nickname, true } @@ -2882,6 +2892,31 @@ func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, return c.sendPresence(args...) } +// achtung: assuming a locked mucState context +func (c *Client) kickStaleOccupant(chatID int64, deleted *MUCOccupant, mucState *MUCState) { + if deleted == nil { + return + } + + // u mad? put me back! + if c.me != nil && c.me.Id == deleted.key { + deleted = mucState.Occupants.Set(deleted.key, deleted) + if deleted == nil { + // WTF but okay + return + } + } + + c.sendPresence( + gateway.SPFrom(gateway.MUCNODE(chatID)), + gateway.SPResource(deleted.Nickname), + gateway.SPType("unavailable"), + gateway.SPMUCAffiliation(deleted.Affiliation), + gateway.SPMUCRole(deleted.Role), + gateway.SPMUCStatusCodes([]uint16{307, 333}), + ) +} + // MigrateToMUCs unsubscribes from legacy group chats and invites to MUCs func (c *Client) MigrateToMUCs() { var chatIDs []int64 @@ -3072,6 +3107,7 @@ func (c *Client) mucOccupantRolePresence(chatID, userID int64, status ChatMember } else { occupant, ok := mucState.Occupants.Get(userID) if ok { + mucState.Occupants.Bump(occupant) occupant.Affiliation = newAffiliation occupant.Role = newRole } From cb6eaef16f656155d006092781f94057ce93b5f4 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 25 Jul 2025 08:22:38 -0400 Subject: [PATCH 185/228] Fix crash on bumping non-existent occupant --- telegram/utils.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index 937ca8b..7cb2b01 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -759,7 +759,9 @@ func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool { } occupant, ok := mucState.Occupants.Get(memberID) - mucState.Occupants.Bump(occupant) + if ok { + mucState.Occupants.Bump(occupant) + } return ok } From b1a67f6f4eda5df61d2e2a35f41bf0bae4da4b40 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 25 Jul 2025 16:30:46 -0400 Subject: [PATCH 186/228] Join/leave a temporary occupant for edit messages with an unrecognized nickname too --- telegram/handlers.go | 49 ++++++++++++++++++++++++++--------------- telegram/utils.go | 32 ++++++++++++++++----------- xmpp/gateway/gateway.go | 9 ++++---- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 0880332..b495423 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -284,6 +284,8 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } if update.NewContent.MessageContentType() == client.TypeMessageText { + safeToSend := true + textContent := update.NewContent.(*client.MessageText) log.Debugf("textContent: %#v", textContent.Text) @@ -321,9 +323,31 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } } + var forceFallback bool + + var from string + var originalFrom string + var nickname string + if isMUC { + if messageErr == nil { + senderId := c.getMessageSenderId(message) + nickname = c.GetMUCNickname(senderId) + originalFrom = gateway.CHATJID(senderId, true) + + safeToSend = c.assureMUCOccupant(update.ChatId, senderId, message.SenderId, chat) + from = gateway.MUCJID(update.ChatId) + "/" + nickname + } else { + nickname = "#ERROR#" + forceFallback = true + from = gateway.MUCJID(update.ChatId) + } + } else { + from = gateway.CHATNODE(update.ChatId) + } + var text strings.Builder - if replaceId == "" { + if replaceId == "" || forceFallback { var editChar string if c.Session.AsciiArrows { editChar = "e" @@ -343,28 +367,17 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { c, )) - var from string - var originalFrom string - if isMUC { - var nickname string - if messageErr == nil { - senderId := c.getMessageSenderId(message) - nickname = c.GetMUCNickname(senderId) - originalFrom = gateway.CHATJID(senderId, true) - } else { - nickname = "#ERROR#" - } - from = gateway.MUCJID(update.ChatId) + "/" + nickname - } else { - from = gateway.CHATNODE(update.ChatId) - } id := "e"+sId uuid, err := uuid.NewRandom() if err == nil { id = id+":"+uuid.String() } for _, jid := range jids { - gateway.SendMessage(jid, from, text.String(), id, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom, "") + if safeToSend { + gateway.SendMessage(jid, from, text.String(), id, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom, "") + } else { + gateway.SendMUCAnnouncement(jid, from, text.String(), nickname, id, c.xmpp) + } } } } @@ -415,7 +428,7 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { nickname = chat.Title } for _, jid := range jids { - gateway.SendMUCAnnouncement(jid, fromJid, text, nickname, c.xmpp) + gateway.SendMUCAnnouncement(jid, fromJid, text, nickname, "", c.xmpp) } } else { fromJid = gateway.CHATNODE(update.ChatId) diff --git a/telegram/utils.go b/telegram/utils.go index 7cb2b01..98fbc3c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -750,6 +750,23 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] ) } +func (c *Client) assureMUCOccupant(chatId, senderId int64, messageSender client.MessageSender, chat *client.Chat) bool { + safeToSend := true + if !c.mucCacheHasOccupant(chatId, senderId) { + chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ + ChatId: chatId, + MemberId: messageSender, + }) + var status client.ChatMemberStatus + if err == nil { + status = chatMember.Status + } + affiliation, role := c.memberStatusToAffiliationAndRole(status, chat) + safeToSend = c.addMUCOccupant(chatId, senderId, affiliation, role, status) + } + return safeToSend +} + func (c *Client) mucCacheHasOccupant(mucID int64, memberID int64) bool { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() @@ -1780,18 +1797,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } } - if !c.mucCacheHasOccupant(chatId, senderId) { - chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ - ChatId: chatId, - MemberId: message.SenderId, - }) - var status client.ChatMemberStatus - if err == nil { - status = chatMember.Status - } - affiliation, role := c.memberStatusToAffiliationAndRole(status, chat) - safeToSend = c.addMUCOccupant(chatId, senderId, affiliation, role, status) - } + safeToSend = c.assureMUCOccupant(chatId, senderId, message.SenderId, chat) groupChatFrom = gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(senderId) var ok bool @@ -2096,7 +2102,7 @@ func (c *Client) returnMessage(returnJid string, chatID int64, text string, code if err == nil { nickname = chat.Title } - gateway.SendMUCAnnouncement(returnJid, gateway.MUCJID(chatID), text, nickname, c.xmpp) + gateway.SendMUCAnnouncement(returnJid, gateway.MUCJID(chatID), text, nickname, "", c.xmpp) } } else { gateway.SendTextMessage(returnJid, gateway.CHATNODE(chatID), text, c.xmpp, isGroupchat) diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 076635a..b3a0cc9 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -111,7 +111,7 @@ func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupch } // SendMUCAnnouncement creates and sends a message by a temporary occupant -func SendMUCAnnouncement(to, from, body, nickname string, component *xmpp.Component) { +func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Component) { if nickname == "" { nickname = "announcement" } @@ -127,9 +127,10 @@ func SendMUCAnnouncement(to, from, body, nickname string, component *xmpp.Compon SPMUCJid(from), ) - var id string - if uuid, err := uuid.NewRandom(); err == nil { - id = uuid.String() + if id == "" { + if uuid, err := uuid.NewRandom(); err == nil { + id = uuid.String() + } } sendMessageWrapper(to, fullFrom, body, "", "", id, component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", 0) From 5ec8eab64b336494f3633f12a7dff8a7e39c8222 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 25 Jul 2025 17:21:38 -0400 Subject: [PATCH 187/228] Send 170 status code when joining supergroups (assuming they are public and basic groups are not) --- telegram/utils.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index 98fbc3c..2426e93 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -695,6 +695,7 @@ func (c *Client) sendMUCStatuses(chatID int64) { } } +// achtung: assuming a locked mucState context func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members []*client.ChatMember) { sChatId := gateway.MUCNODE(chatID) myNickname := "me" @@ -738,6 +739,10 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] } // according to the spec, own occupant entry should be sent the last + selfStatusCodes := []uint16{100, 110, 210} + if chat != nil && chat.Type.ChatTypeType() == client.TypeChatTypeSupergroup { + selfStatusCodes = append(selfStatusCodes, 170) + } c.sendPresence( gateway.SPFrom(sChatId), gateway.SPResource(myNickname), @@ -745,7 +750,7 @@ func (c *Client) updateMUCOccupants(mucState *MUCState, chatID int64, members [] gateway.SPMUCAffiliation(myAffiliation), gateway.SPMUCRole(myRole), gateway.SPMUCJid(myJid), - gateway.SPMUCStatusCodes([]uint16{100, 110, 210}), + gateway.SPMUCStatusCodes(selfStatusCodes), gateway.SPToJids(toJids), ) } From e85b534e4f9ca24e64800f5da604e6b7415f8940 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 28 Jul 2025 20:44:21 -0400 Subject: [PATCH 188/228] Improve detection of nickname change presences --- xmpp/handlers.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 52f0071..07a8eec 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -689,18 +689,24 @@ func tryHandleMUCPresence(s xmpp.Sender, p stanza.Presence) { switch p.Type { case "": - handleMUCNicknameChange(component, p, session, chatId, toBare) + if p.Show == "" { + handleMUCNicknameChange(component, p, session, chatId, toBare, nickname) + } case stanza.PresenceTypeUnavailable: handleMUCUnavailable(component, p, session, chatId, fromResource) } } -func handleMUCNicknameChange(component *xmpp.Component, p stanza.Presence, session *telegram.Client, chatId int64, toBare string) { +func handleMUCNicknameChange(component *xmpp.Component, p stanza.Presence, session *telegram.Client, chatId int64, toBare string, newNickname string) { log.Warn("🗿 Yes") from := toBare nickname, ok := session.GetMyMUCNickname(chatId) if ok { + if nickname == newNickname { + log.Warn("But whatever, it's the same") + return + } from = from+"/"+nickname } reply := &stanza.Presence{ From 0e8d34aafa6d7e6ba5753d9e37ec97d442600b74 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 30 Jul 2025 12:59:13 -0400 Subject: [PATCH 189/228] Validate MUC nicknames via resourceprep --- go.mod | 2 ++ go.sum | 19 +++++++++++++++++++ telegram/utils.go | 24 +++++++++++++++++++++++- xmpp/gateway/gateway.go | 26 ++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 4eb2643..5a97cec 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.4.2 github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e + github.com/xdg-go/stringprep v1.0.4 github.com/zelenin/go-tdlib v0.5.2 gopkg.in/yaml.v2 v2.2.4 gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 @@ -29,6 +30,7 @@ require ( go.opencensus.io v0.22.5 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect nhooyr.io/websocket v1.6.5 // indirect ) diff --git a/go.sum b/go.sum index 82e391a..93efe73 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,11 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zelenin/go-tdlib v0.5.2 h1:inEATEM0Pz6/HBI3wTlhd+brDHpmoXGgwdSb8/V6GiA= github.com/zelenin/go-tdlib v0.5.2/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= @@ -131,6 +134,7 @@ golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -138,6 +142,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -148,6 +153,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -157,6 +164,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -170,11 +178,21 @@ golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -185,6 +203,7 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/telegram/utils.go b/telegram/utils.go index 2426e93..596c0d0 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -853,7 +853,29 @@ func (c *Client) GetMUCNickname(chatID int64) string { return "me" } } - return c.FormatContact(chatID) + fc := c.FormatContact(chatID) + rp, err := gateway.ResourcePrep(fc) + if err != nil { + log.Warnf("Resourceprep for %v failed, falling back to chat ID", fc) + + var usernames string + _, user, _ := c.GetContactByID(chatID, nil) + if user != nil && user.Usernames != nil { + usernames = c.usernamesToString(user.Usernames.ActiveUsernames) + } + + nickname := strconv.FormatInt(chatID, 10) + if usernames != "" { + nickname = fmt.Sprintf("%s (%v)", nickname, usernames) + } else { + nickname = fmt.Sprintf("(%s)", nickname) + } + return nickname + } + if rp != fc { + log.Debugf("Corrected resource: %v -> %v", fc, rp) + } + return rp } func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index b3a0cc9..c37c0ba 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/soheilhy/args" + "github.com/xdg-go/stringprep" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) @@ -87,6 +88,31 @@ func MUCJID(chatId int64) string { return "c" + CHATJID(chatId, false) } +var resourcePrepProfile = stringprep.Profile{ + Mappings: []stringprep.Mapping{ + stringprep.TableB1, + }, + Normalize: true, + Prohibits: []stringprep.Set{ + stringprep.TableC1_2, + stringprep.TableC2_1, + stringprep.TableC2_2, + stringprep.TableC3, + stringprep.TableC4, + stringprep.TableC5, + stringprep.TableC6, + stringprep.TableC7, + stringprep.TableC8, + stringprep.TableC9, + }, + CheckBiDi: true, +} + +// ResourcePrep normalizes a resource according to RFC 6122 +func ResourcePrep(resource string) (string, error) { + return resourcePrepProfile.Prepare(resource) +} + // SendMessage creates and sends a message stanza func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) From 3a18e9dc80ab549c9e95fa9bb3a63071283c3958 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 31 Jul 2025 10:07:23 -0400 Subject: [PATCH 190/228] Occupants LRU refactoring: key -> id --- telegram/handlers.go | 2 +- telegram/muc.go | 22 +++++++++++----------- telegram/utils.go | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index b495423..1185f2f 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -551,7 +551,7 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) { gateway.SPFrom(gateway.MUCNODE(update.ChatId)), gateway.SPResource(occupant.Nickname), gateway.SPImmed(true), - gateway.SPMUCJid(gateway.CHATJID(occupant.key, true)), + gateway.SPMUCJid(gateway.CHATJID(occupant.id, true)), gateway.SPMUCAffiliation(affiliation), gateway.SPMUCRole(role), gateway.SPToJids(toJids), diff --git a/telegram/muc.go b/telegram/muc.go index 9021de5..4f718d9 100644 --- a/telegram/muc.go +++ b/telegram/muc.go @@ -23,7 +23,7 @@ type MUCOccupant struct { Status client.ChatMemberStatus prev *MUCOccupant next *MUCOccupant - key int64 + id int64 } func (o *MUCOccupant) cutOut() (prev, next *MUCOccupant) { @@ -63,11 +63,11 @@ func NewMUCOccupantsLRU() *MUCOccupantsLRU { } } -func (lru *MUCOccupantsLRU) Get(key int64) (*MUCOccupant, bool) { +func (lru *MUCOccupantsLRU) Get(id int64) (*MUCOccupant, bool) { lru.lock.Lock() defer lru.lock.Unlock() - occupant, ok := lru.m[key] + occupant, ok := lru.m[id] return occupant, ok } @@ -91,14 +91,14 @@ func (lru *MUCOccupantsLRU) insertNewest(occupant *MUCOccupant) { } // Set adds or replaces an occupant and possibly returns an occupant removed instead because of overflow -func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) (deleted *MUCOccupant) { +func (lru *MUCOccupantsLRU) Set(id int64, occupant *MUCOccupant) (deleted *MUCOccupant) { lru.lock.Lock() defer lru.lock.Unlock() - occupant.key = key + occupant.id = id - oldOccupant, oldOk := lru.m[key] - lru.m[key] = occupant + oldOccupant, oldOk := lru.m[id] + lru.m[id] = occupant if oldOk { lru.cutOut(oldOccupant) @@ -119,7 +119,7 @@ func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) (deleted *MUCO if len(lru.m) > int(MUCOccupantsLimit) && lru.oldest != nil { deleted = lru.oldest - delete(lru.m, lru.oldest.key) + delete(lru.m, lru.oldest.id) lru.cutOut(lru.oldest) } @@ -127,12 +127,12 @@ func (lru *MUCOccupantsLRU) Set(key int64, occupant *MUCOccupant) (deleted *MUCO } // Delete occupant by member ID -func (lru *MUCOccupantsLRU) Delete(key int64) { +func (lru *MUCOccupantsLRU) Delete(id int64) { lru.lock.Lock() defer lru.lock.Unlock() - oldOccupant, oldOk := lru.m[key] - delete(lru.m, key) + oldOccupant, oldOk := lru.m[id] + delete(lru.m, id) if oldOk { lru.cutOut(oldOccupant) diff --git a/telegram/utils.go b/telegram/utils.go index 596c0d0..08f8d7e 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -996,7 +996,7 @@ func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { for occupant := range mucState.Occupants.Range() { if occupant.Nickname == nickname { - return occupant.key + return occupant.id } } @@ -2934,8 +2934,8 @@ func (c *Client) kickStaleOccupant(chatID int64, deleted *MUCOccupant, mucState } // u mad? put me back! - if c.me != nil && c.me.Id == deleted.key { - deleted = mucState.Occupants.Set(deleted.key, deleted) + if c.me != nil && c.me.Id == deleted.id { + deleted = mucState.Occupants.Set(deleted.id, deleted) if deleted == nil { // WTF but okay return From ca75d3816928c9c0bdd30a18ab1186121f14b6d5 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 31 Jul 2025 10:10:27 -0400 Subject: [PATCH 191/228] Occupants LRU: cached reverse id-by-nickname lookups --- telegram/muc.go | 18 +++++++++++++++++- telegram/utils.go | 9 ++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/telegram/muc.go b/telegram/muc.go index 4f718d9..0470f7d 100644 --- a/telegram/muc.go +++ b/telegram/muc.go @@ -52,6 +52,7 @@ func NewMUCState() *MUCState { type MUCOccupantsLRU struct { m map[int64]*MUCOccupant + rev map[string]int64 oldest *MUCOccupant newest *MUCOccupant lock sync.Mutex @@ -59,7 +60,8 @@ type MUCOccupantsLRU struct { func NewMUCOccupantsLRU() *MUCOccupantsLRU { return &MUCOccupantsLRU{ - m: make(map[int64]*MUCOccupant), + m: make(map[int64]*MUCOccupant), + rev: make(map[string]int64), } } @@ -71,6 +73,14 @@ func (lru *MUCOccupantsLRU) Get(id int64) (*MUCOccupant, bool) { return occupant, ok } +func (lru *MUCOccupantsLRU) GetIdByNickname(nickname string) (int64, bool) { + lru.lock.Lock() + defer lru.lock.Unlock() + + id, ok := lru.rev[nickname] + return id, ok +} + func (lru *MUCOccupantsLRU) cutOut(oldOccupant *MUCOccupant) (prev, next *MUCOccupant) { prev, next = oldOccupant.cutOut() if lru.oldest == oldOccupant { @@ -102,6 +112,9 @@ func (lru *MUCOccupantsLRU) Set(id int64, occupant *MUCOccupant) (deleted *MUCOc if oldOk { lru.cutOut(oldOccupant) + + delete(lru.rev, oldOccupant.Nickname) + lru.rev[occupant.Nickname] = id } if (lru.oldest == nil) != (lru.newest == nil) { @@ -120,6 +133,7 @@ func (lru *MUCOccupantsLRU) Set(id int64, occupant *MUCOccupant) (deleted *MUCOc if len(lru.m) > int(MUCOccupantsLimit) && lru.oldest != nil { deleted = lru.oldest delete(lru.m, lru.oldest.id) + delete(lru.rev, lru.oldest.Nickname) lru.cutOut(lru.oldest) } @@ -136,6 +150,7 @@ func (lru *MUCOccupantsLRU) Delete(id int64) { if oldOk { lru.cutOut(oldOccupant) + delete(lru.rev, oldOccupant.Nickname) } } @@ -183,6 +198,7 @@ func (lru *MUCOccupantsLRU) Clear() { occupant.next = nil } lru.m = make(map[int64]*MUCOccupant) + lru.rev = make(map[string]int64) lru.oldest = nil lru.newest = nil diff --git a/telegram/utils.go b/telegram/utils.go index 08f8d7e..c736f1c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -984,7 +984,7 @@ func (c *Client) GetMyMUCNickname(chatID int64) (string, bool) { return occupant.Nickname, true } -// GetMUCMemberIdByNickname looks up the telegram ID by the MUC nickname (slow yet! (TODO)) +// GetMUCMemberIdByNickname looks up the telegram ID by the MUC nickname func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() @@ -994,10 +994,9 @@ func (c *Client) GetMUCMemberIdByNickname(chatID int64, nickname string) int64 { return 0 } - for occupant := range mucState.Occupants.Range() { - if occupant.Nickname == nickname { - return occupant.id - } + id, ok := mucState.Occupants.GetIdByNickname(nickname) + if ok { + return id } return 0 From b2c4ade6b9e399bcbcf577de39bada6a675d4c8a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 31 Jul 2025 10:25:42 -0400 Subject: [PATCH 192/228] Support vCards for occupant JIDs --- xmpp/handlers.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 07a8eec..15fcd10 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -761,12 +761,24 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { return } - toID, toOk, _ := toToID(iq.To) + toID, toOk, isGroup := toToID(iq.To) if !toOk { log.Error("Invalid IQ to") return } + if isGroup { + toJid, err := stanza.NewJid(iq.To) + if err != nil { + log.Error("Invalid to JID!") + return + } + + if toJid.Resource != "" { + toID = session.GetMUCMemberIdByNickname(toID, toJid.Resource) + } + } + info, err := session.GetVcardInfo(toID) if err != nil { log.Error(err) From 47179e9c77fdf88ed813a801a38e391b53773a7b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 31 Jul 2025 12:51:25 -0400 Subject: [PATCH 193/228] Move to new MUC on upgrading basic group to supergroup --- telegram/connect.go | 2 +- telegram/utils.go | 31 +++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/telegram/connect.go b/telegram/connect.go index afdf5f3..bee3366 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -240,7 +240,7 @@ func (c *Client) Disconnect(resource string, quit bool) bool { if c.Session.MUC { c.locks.mucCacheLock.Lock() for chatID := range c.mucCache { - c.kickMeFromMUC(chatID, []uint16{110, 332}, false, nil) + c.kickMeFromMUC(chatID, []uint16{110, 332}, false, c.mucCache[chatID]) } c.locks.mucCacheLock.Unlock() } diff --git a/telegram/utils.go b/telegram/utils.go index c736f1c..367fda8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -661,19 +661,22 @@ func (c *Client) DestroyMUC(chatId int64) error { return err } + c.deleteMUC(chatId, nil, true) + + return nil +} + +func (c *Client) deleteMUC(chatId int64, statusCodes []uint16, destroy bool) { c.locks.mucCacheLock.Lock() defer c.locks.mucCacheLock.Unlock() mucState, ok := c.mucCache[chatId] if !ok || mucState == nil { - return nil + return } - c.kickMeFromMUC(chatId, nil, true, mucState) - + c.kickMeFromMUC(chatId, statusCodes, destroy, mucState) delete(c.mucCache, chatId) - - return nil } func (c *Client) sendMUCStatuses(chatID int64) { @@ -1817,9 +1820,16 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { case client.TypeMessageChatDeleteMember: deleteMember, _ := message.Content.(*client.MessageChatDeleteMember) c.mucOccupantRolePresence(chatId, deleteMember.UserId, ChatMemberStatusKicked, c.GetMUCNickname(deleteMember.UserId)) - case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate: + case client.TypeMessageBasicGroupChatCreate, client.TypeMessageSupergroupChatCreate, client.TypeMessageChatUpgradeTo: + inviteChatId := chatId + if message.Content.MessageContentType() == client.TypeMessageChatUpgradeTo { + c.deleteMUC(chatId, nil, true) + + upgradeTo, _ := message.Content.(*client.MessageChatUpgradeTo) + inviteChatId = -1000000000000 - upgradeTo.SupergroupId // 🫃 + } for _, jid := range c.GetCarbonFullJids(true, "", false) { - gateway.InviteToMUC(chatId, jid, c.xmpp) + gateway.InviteToMUC(inviteChatId, jid, c.xmpp) } } @@ -2908,7 +2918,7 @@ func (c *Client) kickMeFromMUC(chatID int64, statusCodes []uint16, destroy bool, if c.me != nil { myJid = gateway.CHATJID(c.me.Id, true) } - _, toJids := c.getMUCJoinedJIDs(chatID, mucState, false) + _, toJids := c.getMUCJoinedJIDs(chatID, mucState, mucState == nil) args := []args.V{ gateway.SPFrom(gateway.MUCNODE(chatID)), gateway.SPResource(c.GetMUCNickname(0)), @@ -2979,13 +2989,10 @@ func (c *Client) MigrateFromMUCs() { chatIDs = append(chatIDs, chat.Id) } - c.locks.mucCacheLock.Lock() for _, chatID := range chatIDs { - c.kickMeFromMUC(chatID, []uint16{110, 332}, false, nil) - delete(c.mucCache, chatID) + c.deleteMUC(chatID, []uint16{110, 332}, false) c.subscribeToID(chatID, nil, false) } - c.locks.mucCacheLock.Unlock() } // SetChatMemberStatus is a handy wrapper for the following TDLib method From 12ac2fb5dddc5aaa51d65a5b37731e6f4272417a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 12 Aug 2025 17:29:13 -0400 Subject: [PATCH 194/228] MAM support in MUCs --- config.yml.example | 1 + config/config.go | 9 +- config_schema.json | 3 + telegram/handlers.go | 2 +- telegram/utils.go | 199 +++++++++++- test/bad_config.yml | 1 + test/good_config.yml | 1 + xmpp/component.go | 2 + xmpp/extensions/extensions.go | 135 +++++++- xmpp/gateway/gateway.go | 81 +++-- xmpp/handlers.go | 595 +++++++++++++++++++++++++++++++++- 11 files changed, 973 insertions(+), 56 deletions(-) diff --git a/config.yml.example b/config.yml.example index b8de1dd..260dc46 100644 --- a/config.yml.example +++ b/config.yml.example @@ -7,6 +7,7 @@ :user: 'www-data' # owner of content files :quota: '256MB' # maximum storage size :tdlib_verbosity: 1 + :mam_threshold: 7 # in days :tdlib: :datadir: './sessions/' :client: diff --git a/config/config.go b/config/config.go index 7c685fb..b214495 100644 --- a/config/config.go +++ b/config/config.go @@ -27,10 +27,11 @@ type XMPPConfig struct { // TelegramConfig is for :telegram: subtree type TelegramConfig struct { - Loglevel string `yaml:":loglevel"` - Content TelegramContentConfig `yaml:":content"` - Verbosity uint8 `yaml:":tdlib_verbosity"` - Tdlib TelegramTdlibConfig `yaml:":tdlib"` + Loglevel string `yaml:":loglevel"` + Content TelegramContentConfig `yaml:":content"` + Verbosity uint8 `yaml:":tdlib_verbosity"` + MAMThreshold uint32 `yaml:":mam_threshold"` + Tdlib TelegramTdlibConfig `yaml:":tdlib"` } // TelegramContentConfig is for :content: subtree diff --git a/config_schema.json b/config_schema.json index ab25307..a58da77 100644 --- a/config_schema.json +++ b/config_schema.json @@ -33,6 +33,9 @@ ":tdlib_verbosity": { "type": "integer" }, + ":mam_threshold": { + "type": "integer" + }, ":tdlib": { "required": [":client"], "type": "object", diff --git a/telegram/handlers.go b/telegram/handlers.go index 1185f2f..0170d5e 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -374,7 +374,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { } for _, jid := range jids { if safeToSend { - gateway.SendMessage(jid, from, text.String(), id, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom, "") + gateway.SendMessage(jid, from, text.String(), id, c.xmpp, nil, 0, replaceId, isCarbon, isMUC, false, originalFrom, "", "", "", nil) } else { gateway.SendMUCAnnouncement(jid, from, text.String(), nickname, id, c.xmpp) } diff --git a/telegram/utils.go b/telegram/utils.go index 367fda8..004e9ef 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -13,6 +13,7 @@ import ( osUser "os/user" "path/filepath" "regexp" + "sort" "strconv" "strings" "time" @@ -1844,7 +1845,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } log.Debugf("groupChatFrom: %v groupChatTos: %#v, safeToSend: %v", groupChatFrom, groupChatTos, safeToSend) if safeToSend { - c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos) + c.SendMessageToGateway(chatId, message, "", false, groupChatFrom, groupChatTos, "") } else { mucJID := gateway.MUCJID(chatId) gateway.SendErrorMessage(c.jid, mucJID, "Cannot show a message", 500, true, c.xmpp) @@ -1852,7 +1853,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } // SendMessageToGateway transfers a message to XMPP side and marks it as read on Telegram side -func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id string, delay bool, groupChatFrom string, groupChatTos []string) { +func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id string, delay bool, groupChatFrom string, groupChatTos []string, mamQueryId string) { var isCarbon bool var jids []string var isGroupchat bool @@ -1998,15 +1999,52 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id timestamp = int64(message.Date) } + var mucUserItem *gateway.MUCUserItem + var mucJID string + if mamQueryId != "" { + chatMember, err := c.client.GetChatMember(&client.GetChatMemberRequest{ + ChatId: chatId, + MemberId: message.SenderId, + }) + var status client.ChatMemberStatus + if err == nil { + status = chatMember.Status + } + chat, err := c.GetChatByID(chatId, nil) + if err == nil { + affiliation, role := c.memberStatusToAffiliationAndRole(status, chat) + mucUserItem = &gateway.MUCUserItem{ + Affiliation: affiliation, + Jid: gateway.CHATJID(chatId, false), + Role: role, + } + } + mucJID = gateway.MUCJID(chatId) + } + for _, jid := range jids { - gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) + gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId, mamQueryId, mucJID, mucUserItem) if auxText != "" { - gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId) + gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId, mamQueryId, mucJID, mucUserItem) } } c.UpdateLastChatMessageId(chatId, sId) } +// SendDelayedMUCMessage is used to send MUC history via the legacy method or MAM +func (c *Client) SendDelayedMUCMessage(chatId int64, message *client.Message, toJid string, mamQueryId string) { + msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatId, message.Id) + c.SendMessageToGateway( + chatId, + message, + msgId, + true, + gateway.MUCJID(chatId) + "/" + c.GetMUCNickname(c.getMessageSenderId(message)), + []string{toJid}, + mamQueryId, + ) +} + // MarkAsRead marks a message as read func (c *Client) MarkAsRead(chatId, messageId int64) { c.client.ViewMessages(&client.ViewMessagesRequest{ @@ -2330,6 +2368,73 @@ func (c *Client) getNLastMessages(chatID int64, limit *MessageLimit) ([]*client. return messages, nil } +// GetMessagesBetween lazily fetches message history between given ids (from exclusive, last inclusive), also calculating completeness flag; negative limit means messages from the end +func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, limit int32) (messages []*client.Message, complete bool, err error) { + log.WithFields(log.Fields{ + "chat_id": chatID, + "from": fromMessageId, + "last": lastMessageId, + "limit": limit, + }).Debug("messages between") + + var newMessages *client.Messages + if limit == 0 { + return + } + + var reqFromMessageId int64 + var reqOffset, reqLimit int32 + if limit < 0 { + reqFromMessageId = lastMessageId + reqOffset = -1 + reqLimit = limit + } else { + reqFromMessageId = fromMessageId + reqOffset = -limit + reqLimit = limit + } + + newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{ + ChatId: chatID, + FromMessageId: reqFromMessageId, + Offset: reqOffset, + Limit: reqLimit, + }) + if err == nil { + if len(newMessages.Messages) == 0 { + complete = true + } + if limit < 0 { + fromPos := -1 + for i, message := range newMessages.Messages { + if message.Id == fromMessageId { + fromPos = i + break + } + } + if fromPos > -1 { + messages = newMessages.Messages[fromPos:] + } else { + messages = newMessages.Messages + } + complete = true + } else { + for _, message := range newMessages.Messages { + if lastMessageId != 0 { + if message.Id > lastMessageId { + break + complete = true + } else if message.Id == lastMessageId { + complete = true + } + } + messages = append(messages, message) + } + } + } + return +} + // 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{ @@ -2751,18 +2856,13 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p false, originalFrom, "", + "", + "", + nil, ) } } else { - msgId, _ := gateway.IdsDB.GetByTgIds(c.Session.Login, c.jid, chatID, message.Id) - c.SendMessageToGateway( - chatID, - message, - msgId, - true, - mucJid + "/" + c.GetMUCNickname(c.getMessageSenderId(message)), - []string{toJid}, - ) + c.SendDelayedMUCMessage(chatID, message, toJid, "") } } } @@ -3072,6 +3172,22 @@ func (c *Client) DeleteChat(chatID int64) error { return err } +// GetMessage is a handy wrapper for the following TDLib method +func (c *Client) GetMessage(chatID, messageId int64) (*client.Message, error) { + return c.client.GetMessage(&client.GetMessageRequest{ + ChatId: chatID, + MessageId: messageId, + }) +} + +// GetChatMessagePosition is a handy wrapper for the following TDLib method +func (c *Client) GetChatMessagePosition(chatID, messageId int64) (*client.Count, error) { + return c.client.GetChatMessagePosition(&client.GetChatMessagePositionRequest{ + ChatId: chatID, + MessageId: messageId, + }) +} + // CloneChatPermissions makes a copy of ChatPermissions structure func CloneChatPermissions(permissions *client.ChatPermissions) *client.ChatPermissions { return &client.ChatPermissions{ @@ -3219,6 +3335,46 @@ func (c *Client) GetMUCNicknameByUsername(username string) (string, error) { return c.GetMUCNickname(chat.Id), nil } +// FindMessageByTime retrieves the closest message before the given timestamp +func (c *Client) FindMessageByTime(chatID int64, ts time.Time) (*client.Message, error) { + if ts.IsZero() { + return nil, nil + } + + return c.client.GetChatMessageByDate(&client.GetChatMessageByDateRequest{ + ChatId: chatID, + Date: int32(ts.Unix()), // DUROV!!!!!!!!!!!!!!!1111111 + }) +} + +// GetNextMessage attempts to obtain the next message in the history +func (c *Client) GetNextMessage(chatID, messageId int64) (*client.Message, error) { + messages, err := c.client.GetChatHistory(&client.GetChatHistoryRequest{ + ChatId: chatID, + FromMessageId: messageId, + Limit: 1, + Offset: -1, + }) + if err == nil && len(messages.Messages) == 1 && messages.Messages[0] != nil && messages.Messages[0].Id != messageId { + return messages.Messages[0], nil + } + return nil, err +} + +// GetPreviousMessage attempts to obtain the previous message in the history +func (c *Client) GetPreviousMessage(chatID, messageId int64) (*client.Message, error) { + messages, err := c.client.GetChatHistory(&client.GetChatHistoryRequest{ + ChatId: chatID, + FromMessageId: messageId, + Limit: 1, + Offset: 0, + }) + if err == nil && len(messages.Messages) == 1 && messages.Messages[0] != nil && messages.Messages[0].Id != messageId { + return messages.Messages[0], nil + } + return nil, err +} + // GetErrorCode obtains an error code from a Telegram response error func GetErrorCode(err error) (int32, bool) { responseError, ok := err.(client.ResponseError) @@ -3227,3 +3383,20 @@ func GetErrorCode(err error) (int32, bool) { } return responseError.Err.Code, true } + +// ChronologicallySortMessages is… self-explanatory (Achtung: destructive) +func ChronologicallySortMessages(messages []*client.Message) []*client.Message { + sort.Slice(messages, func(i int, j int) bool { + msg1 := messages[i] + msg2 := messages[j] + return msg1.Date < msg2.Date + }) + return messages +} + +// ReverseMessagesSlice efficiently reverses a messages slice in-place +func ReverseMessagesSlice(s []*client.Message) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} diff --git a/test/bad_config.yml b/test/bad_config.yml index 094faf4..0b554df 100644 --- a/test/bad_config.yml +++ b/test/bad_config.yml @@ -5,6 +5,7 @@ :link: 'http://tlgrm.localhost/content' # webserver public address :upload: 'https:///xmppfiles.localhost' # xmpp http upload address :tdlib_verbosity: 1 + :mam_threshold: 7 # in days :tdlib: :client: :api_id: '17349' diff --git a/test/good_config.yml b/test/good_config.yml index ac68438..547eb7e 100644 --- a/test/good_config.yml +++ b/test/good_config.yml @@ -5,6 +5,7 @@ :link: '' # webserver public address :upload: '' # xmpp http upload address :tdlib_verbosity: 1 + :tdlib_verbosity: 7 # in days :tdlib: :client: :api_id: '17349' diff --git a/xmpp/component.go b/xmpp/component.go index f0c481d..45c5440 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -65,6 +65,8 @@ func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath stri } } + gateway.MAMThreshold = tc.MAMThreshold + options := xmpp.ComponentOptions{ TransportConfiguration: xmpp.TransportConfiguration{ Address: conf.Host + ":" + conf.Port, diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 04d13d2..ba03643 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -3,6 +3,7 @@ package extensions import ( "encoding/xml" "strconv" + "time" "gosrc.io/xmpp/stanza" ) @@ -224,6 +225,7 @@ type MessageXMucUserExtension struct { XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` Invite *MessageXMucUserInvite `xml:"invite,omitempty"` Status *MessageXMucUserStatus `xml:"status,omitempty"` + Item PresenceXMucUserItem `xml:"item,omitempty"` Password string `xml:"password,omitempty"` } @@ -273,10 +275,21 @@ type PresenceXMucUserStatus struct { // MessageDelay is from XEP-0203 type MessageDelay struct { XMLName xml.Name `xml:"urn:xmpp:delay delay"` - From string `xml:"from,attr"` + From string `xml:"from,attr,omitempty"` Stamp string `xml:"stamp,attr"` } +func NewMessageDelay(timestamp int64, from string) MessageDelay { + return MessageDelay{ + From: from, + Stamp: TimestampToRFC3339(timestamp), + } +} + +func TimestampToRFC3339(timestamp int64) string { + return time.Unix(timestamp, 0).UTC().Format(time.RFC3339) +} + // MessageDelayLegacy is from XEP-0203 type MessageDelayLegacy struct { XMLName xml.Name `xml:"jabber:x:delay x"` @@ -284,6 +297,13 @@ type MessageDelayLegacy struct { Stamp string `xml:"stamp,attr"` } +func NewMessageDelayLegacy(timestamp int64, from string) MessageDelayLegacy { + return MessageDelayLegacy{ + From: from, + Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"), + } +} + // MessageAddresses is from XEP-0033 type MessageAddresses struct { XMLName xml.Name `xml:"http://jabber.org/protocol/address addresses"` @@ -347,6 +367,65 @@ type MucDestroy struct { Reason string `xml:"reason,omitempty"` } +// MAM2Query is from XEP-0313 +type MAM2Query struct { + XMLName xml.Name `xml:"urn:xmpp:mam:2 query"` + Form *stanza.Form `xml:"jabber:x:data x"` + QueryId string `xml:"queryid,attr,omitempty"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` + FlipPage *FlipPage `xml:"flip-page"` +} + +// FlipPage is an extended element from XEP-0313 +type FlipPage struct { + XMLName xml.Name `xml:"flip-page"` +} + +// ForwardedMessage is from XEP-0297 (go-xmpp lacks Delay) +type ForwardedMessage struct { + XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"` + Message *stanza.Message `xml:"message"` + Delay *MessageDelay `xml:"urn:xmpp:delay delay,omitempty"` +} + +// MAM2MessageResult is from XEP-0313 +type MAM2MessageResult struct { + XMLName xml.Name `xml:"urn:xmpp:mam:2 result"` + Forwarded *ForwardedMessage `xml:"urn:xmpp:forward:0 forwarded,omitempty"` + QueryId string `xml:"queryid,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` +} + +// MAM2Fin is from XEP-0313 +type MAM2Fin struct { + XMLName xml.Name `xml:"urn:xmpp:mam:2 fin"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` + Complete bool `xml:"complete,attr,omitempty"` + Stable bool `xml:"stable,attr"` +} + +// MAM2Metadata is from XEP-0313 +type MAM2Metadata struct { + XMLName xml.Name `xml:"urn:xmpp:mam:2 metadata"` + Start *MAM2MetadataStart `xml:"start"` + End *MAM2MetadataEnd `xml:"end"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + +// MAM2MetadataStart is from XEP-0313 +type MAM2MetadataStart struct { + XMLName xml.Name `xml:"start"` + Id string `xml:"id,attr,omitempty"` + Timestamp string `xml:"timestamp,attr,omitempty"` +} + +// MAM2MetadataEnd is from XEP-0313 +type MAM2MetadataEnd struct { + XMLName xml.Name `xml:"end"` + Id string `xml:"id,attr,omitempty"` + Timestamp string `xml:"timestamp,attr,omitempty"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -452,6 +531,36 @@ func (c QueryMucOwner) GetSet() *stanza.ResultSet { return c.ResultSet } +// Namespace is a namespace! +func (c MAM2Query) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c MAM2Query) GetSet() *stanza.ResultSet { + return c.ResultSet +} + +// Namespace is a namespace! +func (c MAM2Fin) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c MAM2Fin) GetSet() *stanza.ResultSet { + return c.ResultSet +} + +// Namespace is a namespace! +func (c MAM2Metadata) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c MAM2Metadata) GetSet() *stanza.ResultSet { + return c.ResultSet +} + // NewReplyFallback initializes a fallback range func NewReplyFallback(start uint64, end uint64) Fallback { return Fallback{ @@ -585,4 +694,28 @@ func init() { "http://jabber.org/protocol/muc#owner", "query", }, QueryMucOwner{}) + + // MAM2 query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:mam:2", + "query", + }, MAM2Query{}) + + // MAM2 message result + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:mam:2", + "result", + }, MAM2MessageResult{}) + + // MAM2 fin + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:mam:2", + "fin", + }, MAM2Fin{}) + + // MAM2 metadata + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:mam:2", + "metadata", + }, MAM2Metadata{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index c37c0ba..4657a2f 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" "sync" - "time" "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/xmpp/extensions" @@ -38,6 +37,12 @@ type marker struct { Id string } +type MUCUserItem struct { + Affiliation string + Jid string + Role string +} + const NSNick string = "http://jabber.org/protocol/nick" const NodeVCard4 string = "urn:xmpp:vcard4" const NodeAvatarMetadata string = "urn:xmpp:avatar:metadata" @@ -62,6 +67,9 @@ var DirtySessions = false // MessageOutgoingPermissionVersion contains a XEP-0356 version to fake outgoing messages by foreign JIDs var MessageOutgoingPermissionVersion = 0 +// MAMThreshold specifies a day limit behind which history should not be requested to avoid abuse detection and storage overload +var MAMThreshold uint32 + // CHATNODE converts numeric id to node part of 1-1 chat JID func CHATNODE(chatId int64) string { return strconv.FormatInt(chatId, 10) @@ -114,8 +122,8 @@ func ResourcePrep(resource string) (string, error) { } // SendMessage creates and sends a message stanza -func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) +func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId, mamQueryId, mucJID string, mucUserItem *MUCUserItem) { + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0, mamQueryId, mucJID, mucUserItem) } // SendServiceMessage creates and sends a simple message stanza from transport @@ -124,7 +132,7 @@ func SendServiceMessage(to, body string, component *xmpp.Component) { if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "", 0) + sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "", 0, "", "", nil) } // SendTextMessage creates and sends a simple message stanza @@ -133,7 +141,7 @@ func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupch if uuid, err := uuid.NewRandom(); err == nil { id = uuid.String() } - sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "", 0) + sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "", 0, "", "", nil) } // SendMUCAnnouncement creates and sends a message by a temporary occupant @@ -158,7 +166,7 @@ func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Co id = uuid.String() } } - sendMessageWrapper(to, fullFrom, body, "", "", id, component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", 0) + sendMessageWrapper(to, fullFrom, body, "", "", id, component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", 0, "", "", nil) SendPresence( component, @@ -173,22 +181,22 @@ func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Co // SendErrorMessage creates and sends an error message stanza func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0) + sendMessageWrapper(to, from, "", "", text, "", component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0, "", "", nil) } // SendErrorMessageWithBody creates and sends an error message stanza with body payload func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) { - sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0) + sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0, "", "", nil) } // SendMessageWithOOB creates and sends a message stanza with OOB URL -func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId string) { - sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0) +func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId, mamQueryId, mucJID string, mucUserItem *MUCUserItem) { + sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0, mamQueryId, mucJID, mucUserItem) } // SendSubjectMessage creates and sends a MUC subject func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) { - sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "", "", 0) + sendMessageWrapper(to, from, "", subject, "", id, component, nil, nil, timestamp, "", "", false, true, true, false, "", 0, "", "", 0, "", "", nil) } // SendMessageMarker creates and sends a message stanza with a XEP-0333 marker @@ -196,20 +204,20 @@ func SendMessageMarker(to string, from string, component *xmpp.Component, marker sendMessageWrapper(to, from, "", "", "", "", component, nil, &marker{ Type: markerType, Id: markerId, - }, 0, "", "", false, false, false, false, "", 0, "", "", 0) + }, 0, "", "", false, false, false, false, "", 0, "", "", 0, "", "", nil) } // SendMUCInvite creates and send a MUC invitation message func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) { - sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "", 0) + sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "", 0, "", "", nil) } // SendMUCStatusCode creates a groupchat message with a muc#user status code func SendMUCStatusCode(to string, from string, component *xmpp.Component, statusCode int64) { - sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", statusCode) + sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", statusCode, "", "", nil) } -func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string, statusCode int64) { +func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string, statusCode int64, mamQueryId, mucJID string, mucUserItem *MUCUserItem) { toJid, err := stanza.NewJid(to) if err != nil { log.WithFields(log.Fields{ @@ -247,6 +255,8 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if isCarbon { messageTo = messageFrom messageFrom = bareTo + "/" + Jid.Resource + } else if mucJID != "" { + messageTo = mucJID } else { messageTo = to } @@ -322,19 +332,13 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component if !isGroupchat && !isCarbon && toJid.Resource != "" && inviteFrom == "" { message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) } - if timestamp != 0 { + if timestamp != 0 && mamQueryId == "" { var delayFrom string if isGroupchat { delayFrom = bareFrom } - message.Extensions = append(message.Extensions, extensions.MessageDelay{ - From: delayFrom, - Stamp: time.Unix(timestamp, 0).UTC().Format(time.RFC3339), - }) - message.Extensions = append(message.Extensions, extensions.MessageDelayLegacy{ - From: delayFrom, - Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"), - }) + message.Extensions = append(message.Extensions, extensions.NewMessageDelay(timestamp, delayFrom)) + message.Extensions = append(message.Extensions, extensions.NewMessageDelayLegacy(timestamp, delayFrom)) } if originalFrom != "" { message.Extensions = append(message.Extensions, extensions.MessageAddresses{ @@ -377,7 +381,14 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component Code: strconv.FormatInt(statusCode, 10), } } - if inviteFrom != "" || statusCode != 0 { + if mucUserItem != nil { + userExt.Item = extensions.PresenceXMucUserItem{ + Affiliation: mucUserItem.Affiliation, + Jid: &mucUserItem.Jid, + Role: mucUserItem.Role, + } + } + if inviteFrom != "" || statusCode != 0 || mucUserItem != nil { message.Extensions = append(message.Extensions, userExt) } if stanzaId != "" { @@ -425,6 +436,26 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component }) } sendMessage(&privilegeMessage, component) + } else if mamQueryId != "" { + delay := extensions.NewMessageDelay(timestamp, "") + mamMessage := stanza.Message{ + Attrs: stanza.Attrs{ + From: mucJID, + To: to, + Type: messageType, + }, + Extensions: []stanza.MsgExtension{ + extensions.MAM2MessageResult{ + Id: stanzaId, + QueryId: mamQueryId, + Forwarded: &extensions.ForwardedMessage{ + Delay: &delay, + Message: &message, + }, + }, + }, + } + sendMessage(&mamMessage, component) } else { sendMessage(&message, component) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 15fcd10..5875367 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -83,6 +83,16 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetQueryMucOwner(s, iq) return } + queryMAM2, ok := iq.Payload.(*extensions.MAM2Query) + if ok { + go handleGetQueryMAM2(s, iq, queryMAM2) + return + } + _, ok = iq.Payload.(*extensions.MAM2Metadata) + if ok { + go handleGetMetadataMAM2(s, iq) + return + } } else if iq.Type == stanza.IQTypeSet { queryRegister, ok := iq.Payload.(*extensions.QueryRegister) if ok { @@ -104,6 +114,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleSetQueryMucOwner(s, iq, queryMucOwner) return } + queryMAM2, ok := iq.Payload.(*extensions.MAM2Query) + if ok { + go handleSetQueryMAM2(s, iq, queryMAM2) + return + } } else if iq.Type == stanza.IQTypeResult { discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { @@ -202,17 +217,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { log.Debugf("replace tg: %#v %#v", chatId, msgId) } } else { - id := reply.Id - if id[0] == 'e' { - idParts := strings.Split(id[1:], ":") - if len(idParts) >= 1 { - id = idParts[0] - } - } - replyId, err = strconv.ParseInt(id, 10, 64) - if err != nil { - log.Warn(errors.Wrap(err, "Failed to parse message ID!")) - } + replyId, _ = parseMessageId(reply.Id) } if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 { @@ -292,11 +297,12 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { false, msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage.SenderId)), []string{msg.From}, + "", ) } } else if isCommand && isGroupchat && session.Session.MUC { // pong outgoing commands back to groupchats - gateway.SendMessage(msg.From, msg.To + "/" + session.GetMUCNickname(0), text, "", component, nil, 0, "", false, isGroupchat, false, "", "") + gateway.SendMessage(msg.From, msg.To + "/" + session.GetMUCNickname(0), text, "", component, nil, 0, "", false, isGroupchat, false, "", "", "", "", nil) } else { /* // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway @@ -970,6 +976,8 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { "muc_unsecured", "http://jabber.org/protocol/muc#stable_id", "jabber:iq:register", + "urn:xmpp:mam:2", + "urn:xmpp:mam:2#extended", "urn:xmpp:sid:0", "vcard-temp", ) @@ -1374,6 +1382,113 @@ func handleGetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ) { } } +func handleGetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Query) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + defer gateway.ResumableSend(component, answer) + + payload := &extensions.MAM2Query{} + answer.Payload = payload + + payload.Form = &stanza.Form{ + Type: stanza.FormTypeForm, + Fields: []*stanza.Field{ + &stanza.Field{ + Var: "FORM_TYPE", + Type: stanza.FieldTypeHidden, + ValuesList: []string{"urn:xmpp:mam:2"}, + }, + &stanza.Field{ + Var: "with", + Type: stanza.FieldTypeJidSingle, + }, + &stanza.Field{ + Var: "start", + Type: stanza.FieldTypeTextSingle, + }, + &stanza.Field{ + Var: "end", + Type: stanza.FieldTypeTextSingle, + }, + &stanza.Field{ + Var: "before-id", + Type: stanza.FieldTypeTextSingle, + }, + &stanza.Field{ + Var: "after-id", + Type: stanza.FieldTypeTextSingle, + }, + &stanza.Field{ + Var: "ids", + Type: stanza.FieldTypeListMulti, + }, + }, + } + log.Debugf("MAM info request: %#v", query) +} + +func handleGetMetadataMAM2(s xmpp.Sender, iq *stanza.IQ) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + defer gateway.ResumableSend(component, answer) + + bare, _, fromOk := gateway.SplitJID(iq.From) + if !fromOk { + iqAnswerSetError(answer, 400) + return + } + + session, sessionOk := sessions[bare] + if !sessionOk || !session.Session.MUC { + iqAnswerSetError(answer, 403) + return + } + + toID, toOk, toIsGroup := toToID(iq.To) + if !toOk || !toIsGroup { + iqAnswerSetError(answer, 405) + return + } + + chat, _, err := session.GetContactByID(toID, nil) + if err != nil || chat == nil || !session.IsGroup(chat) { + iqAnswerSetError(answer, 405) + return + } + + payload := &extensions.MAM2Metadata{} + answer.Payload = payload + + quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold)) + preFirstMessage, preFirstMessageErr := session.FindMessageByTime(toID, quotaTs) + var preFirstMessageId int64 + if preFirstMessageErr == nil && preFirstMessage != nil { + preFirstMessageId = preFirstMessage.Id + } else { + preFirstMessageId = 1 + } + startMessage, startMessageErr := session.GetNextMessage(toID, preFirstMessageId) + if startMessageErr == nil && startMessage != nil { + payload.Start = &extensions.MAM2MetadataStart{ + Id: strconv.FormatInt(startMessage.Id, 10), + Timestamp: extensions.TimestampToRFC3339(int64(startMessage.Date)), + } + } + + endMessage, endMessageErr := session.GetPreviousMessage(toID, 0) + if endMessageErr == nil && endMessage != nil { + payload.End = &extensions.MAM2MetadataEnd{ + Id: strconv.FormatInt(endMessage.Id, 10), + Timestamp: extensions.TimestampToRFC3339(int64(endMessage.Date)), + } + } + log.Debugf("MAM metadata: %#v", payload) +} + func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { component, answer, ok := iqResultStub(s, iq) if !ok { @@ -2039,6 +2154,442 @@ func handleSetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } +func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Query) { + component, answer, ok := iqResultStub(s, iq) + if !ok { + return + } + defer gateway.ResumableSend(component, answer) + + bare, _, fromOk := gateway.SplitJID(iq.From) + if !fromOk { + iqAnswerSetError(answer, 400) + return + } + + session, sessionOk := sessions[bare] + if !sessionOk || !session.Session.MUC { + iqAnswerSetError(answer, 403) + return + } + + toID, toOk, toIsGroup := toToID(iq.To) + if !toOk || !toIsGroup { + iqAnswerSetError(answer, 405) + return + } + + chat, _, err := session.GetContactByID(toID, nil) + if err != nil || chat == nil || !session.IsGroup(chat) { + iqAnswerSetError(answer, 405) + return + } + + var startTime, endTime time.Time + var beforeId, afterId int64 + var ids []string + + var rsmBefore, rsmAfter int64 + var rsmLastPage bool + var rsmLimit int32 + var justCount bool + + log.Debugf("MAM query to %v: %#v %#v %#v", toID, query, query.Form, query.ResultSet) + if query.Form != nil && query.Form.Type == stanza.FormTypeSubmit { + for _, field := range query.Form.Fields { + if len(field.ValuesList) < 1 { + iqAnswerSetError(answer, 400) + return + } + + value := field.ValuesList[0] + log.Debugf("MAM query field: %v %v", field.Var, value) + + switch field.Var { + case "FORM_TYPE": + if value != "urn:xmpp:mam:2" { + iqAnswerSetError(answer, 400) + return + } + case "with": // okay, and? + case "start", "end": + timestamp, err := time.Parse(time.RFC3339, value) + if err != nil { + iqAnswerSetError(answer, 400) + return + } + + switch field.Var { + case "start": + startTime = timestamp + case "end": + endTime = timestamp + } + case "before-id": + beforeId, ok = parseMessageId(value) + if !ok { + iqAnswerSetError(answer, 400) + return + } + case "after-id": + afterId, ok = parseMessageId(value) + if !ok { + iqAnswerSetError(answer, 400) + return + } + case "ids": + ids = field.ValuesList + default: + iqAnswerSetError(answer, 501) + return + } + } + } + + if query.ResultSet != nil { + if query.ResultSet.After != nil { + rsmAfter, ok = parseMessageId(*query.ResultSet.After) + if !ok { + iqAnswerSetError(answer, 400) + return + } + log.Debugf("MAM RSM after: %v", rsmAfter) + } + if query.ResultSet.Before != nil { + before := *query.ResultSet.Before + if before == "" { + rsmLastPage = true + } else { + rsmBefore, ok = parseMessageId(*query.ResultSet.Before) + if !ok { + iqAnswerSetError(answer, 400) + return + } + log.Debugf("MAM RSM after: %v", rsmBefore) + } + } + if query.ResultSet.Max != nil { + rsmLimit = int32(*query.ResultSet.Max) + if rsmLimit == 0 { + justCount = true + } + log.Debugf("MAM RSM max: %v", rsmLimit) + } + + if query.ResultSet.First != nil { + iqAnswerSetError(answer, 400) + return + } + if query.ResultSet.Index != nil { + iqAnswerSetError(answer, 501) + return + } + if query.ResultSet.Last != nil { + iqAnswerSetError(answer, 400) + return + } + } + + if rsmLimit == 0 && !justCount { + rsmLimit = 100 + } + + if rsmLastPage { + rsmLimit = -rsmLimit // hacky, I know, and? :P + } + + // check for mutual parameter compatibility, there's a lot of them, nah? + if ((!startTime.IsZero() || !endTime.IsZero()) && (beforeId != 0 || afterId != 0 || ids != nil)) || + ((beforeId != 0 || afterId != 0) && (!startTime.IsZero() || !endTime.IsZero() || ids != nil)) || + (ids != nil && (!startTime.IsZero() || !endTime.IsZero() || beforeId != 0 || afterId != 0)) { + iqAnswerSetError(answer, 501) + return + } + + // dummy call to circumvent unexported type + messages, _, err := session.GetMessagesBetween(toID, 0, 0, 0) + + quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold)) + var beyond, complete bool + canBeComplete := true + fromStart := true + var overallyFirstMessageId, overallyLastMessageId int64 + + if ids != nil { + for _, sId := range ids { + id, ok := parseMessageId(sId) + if !ok { + iqAnswerSetError(answer, 400) + return + } + msg, err := session.GetMessage(toID, id) + if err != nil { + iqAnswerSetError(answer, 404) + return + } + messages = append(messages, msg) + } + messages = telegram.ChronologicallySortMessages(messages) + complete = true + } else if beforeId != 0 || afterId != 0 { + if (beforeId != 0 && afterId != 0) && beforeId > afterId { + iqAnswerSetError(answer, 400) + return + } + + // don't allow to fetch far beyond the quota + var replaceWithQuotaTs bool + if afterId != 0 { + afterMessage, afterMessageErr := session.GetMessage(toID, afterId) + if afterMessageErr == nil && afterMessage != nil { + if int64(afterMessage.Date) < quotaTs.Unix() { + replaceWithQuotaTs = true + } + } else { + iqAnswerSetError(answer, 404) + return + } + } else { + replaceWithQuotaTs = true + } + if replaceWithQuotaTs { + preFirstMessage, preFirstMessageErr := session.FindMessageByTime(toID, quotaTs) + if preFirstMessageErr == nil && preFirstMessage != nil { + afterId = preFirstMessage.Id + } else { + // there seem to be no messages older than quota, it's safe to fetch from the very start + afterId = 1 // https://github.com/tdlib/td/issues/195#issuecomment-380836359 + } + } + + if beforeId != 0 { + beforeMessage, beforeMessageErr := session.GetMessage(toID, beforeId) + if beforeMessageErr != nil || beforeMessage == nil { + iqAnswerSetError(answer, 404) + return + } + } + + if rsmAfter != 0 { + if rsmAfter < afterId || (beforeId != 0 && rsmAfter > beforeId) { + iqAnswerSetError(answer, 400) + return + } + if rsmAfter != afterId { + fromStart = false + overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, afterId) + if overallyFirstMessageErr == nil && overallyFirstMessage != nil { + overallyFirstMessageId = overallyFirstMessage.Id + } + } + afterId = rsmAfter + } + + if rsmBefore != 0 { + if rsmBefore < afterId || (beforeId != 0 && rsmBefore > beforeId) { + iqAnswerSetError(answer, 400) + return + } + if rsmBefore < beforeId { + canBeComplete = false + overallyLastMessageId = beforeId + } + beforeId = rsmBefore + } + + var lastMessageId int64 + // should be fine even with afterId=0 as it would mean the last as needed + lastMessage, lastMessageErr := session.GetPreviousMessage(toID, afterId) + if lastMessageErr == nil && lastMessage != nil { + lastMessageId = lastMessage.Id + } else { + beyond = true + } + + if !beyond { + var newComplete bool + messages, newComplete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit) + if canBeComplete { + complete = newComplete + if !complete { + if lastMessageId != 0 { + overallyLastMessageId = lastMessageId + } else { + newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) + if newestMessageErr == nil && newestMessage != nil { + overallyLastMessageId = newestMessage.Id + } + } + } + } + } + } else { // time limit or no limits at all + // don't allow to fetch far beyond the quota + if !endTime.IsZero() && endTime.Before(quotaTs) { + beyond = true + } + if (!startTime.IsZero() && startTime.Before(quotaTs)) || startTime.IsZero() { + startTime = quotaTs + } + + if !beyond { + var fromMessageId int64 + var lastMessageId int64 + fromMessage, fromMessageErr := session.FindMessageByTime(toID, startTime) + if fromMessageErr == nil && fromMessage != nil { + fromMessageId = fromMessage.Id + } else { + // there seem to be no messages older than quota, it's safe to fetch from the very start + fromMessageId = 1 // https://github.com/tdlib/td/issues/195#issuecomment-380836359 + } + + if !endTime.IsZero() { + endMsg, endMsgErr := session.FindMessageByTime(toID, endTime) + if endMsgErr == nil && endMsg != nil { + lastMessageId = endMsg.Id + } else { + beyond = true + } + } + + if rsmAfter != 0 { + rsmAfterMessage, rsmAfterMessageErr := session.GetMessage(toID, rsmAfter) + if rsmAfterMessageErr == nil && rsmAfterMessage != nil { + if int64(rsmAfterMessage.Date) < startTime.Unix() || (!endTime.IsZero() && int64(rsmAfterMessage.Date) > endTime.Unix()) { + iqAnswerSetError(answer, 400) + return + } + + if rsmAfterMessage.Id > fromMessageId { + fromStart = false + overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, fromMessageId) + if overallyFirstMessageErr == nil && overallyFirstMessage != nil { + overallyFirstMessageId = overallyFirstMessage.Id + } + } + fromMessageId = rsmAfterMessage.Id + } else { + iqAnswerSetError(answer, 404) + return + } + } + + if rsmBefore != 0 { + rsmBeforeMessage, rsmBeforeMessageErr := session.GetMessage(toID, rsmBefore) + if rsmBeforeMessageErr == nil && rsmBeforeMessage != nil { + if int64(rsmBeforeMessage.Date) < startTime.Unix() || (!endTime.IsZero() && int64(rsmBeforeMessage.Date) > endTime.Unix()) { + iqAnswerSetError(answer, 400) + return + } + + newLastMessage, newLastMessageErr := session.GetPreviousMessage(toID, rsmBeforeMessage.Id) + if newLastMessageErr == nil && newLastMessage != nil { + if lastMessageId == 0 || lastMessageId != newLastMessage.Id { + canBeComplete = false + if lastMessageId != 0 { + overallyLastMessageId = lastMessageId + } else { + newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) + if newestMessageErr == nil && newestMessage != nil { + overallyLastMessageId = newestMessage.Id + } + } + lastMessageId = newLastMessage.Id + } + } else { + // nothing?.. not complete, just empty + beyond = true + } + } else { + iqAnswerSetError(answer, 404) + return + } + } + + if !beyond { // yes🗿, twice + var newComplete bool + messages, newComplete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit) + if canBeComplete { + complete = newComplete + if !complete { + if lastMessageId != 0 { + overallyLastMessageId = lastMessageId + } else { + newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) + if newestMessageErr == nil && newestMessage != nil { + overallyLastMessageId = newestMessage.Id + } + } + } + } + } + } + } + + if query.FlipPage != nil { + telegram.ReverseMessagesSlice(messages) + } + + log.Debugf("obtained %v messages", len(messages)) + for _, message := range messages { + session.SendDelayedMUCMessage(toID, message, iq.From, query.QueryId) + } + + rs := stanza.ResultSet{} + answer.Payload = &extensions.MAM2Fin{ + ResultSet: &rs, + Complete: complete, + Stable: false, + } + + if beyond { + count := 0 + rs.Count = &count + } else { + if len(messages) > 0 { + if fromStart { + overallyFirstMessageId = messages[0].Id + } + if complete { + overallyLastMessageId = messages[len(messages)-1].Id + } + } + + var firstMsgPositionCount, lastMsgPositionCount int32 + // estimate overall count + if overallyFirstMessageId != 0 && overallyLastMessageId != 0 { + firstMsgPosition, firstMsgPositionErr := session.GetChatMessagePosition(toID, overallyFirstMessageId) + if firstMsgPositionErr == nil && firstMsgPosition != nil { + firstMsgPositionCount = firstMsgPosition.Count + } + lastMsgPosition, lastMsgPositionErr := session.GetChatMessagePosition(toID, overallyLastMessageId) + if lastMsgPositionErr == nil && lastMsgPosition != nil { + lastMsgPositionCount = lastMsgPosition.Count + } + if firstMsgPositionCount != 0 && lastMsgPositionCount != 0 { + count := int(lastMsgPositionCount - firstMsgPositionCount + 1) + rs.Count = &count + } + } + + if len(messages) > 0 { + rs.First = &stanza.First{ + Content: strconv.FormatInt(messages[0].Id, 10), + } + if firstMsgPositionCount != 0 { + rsmFirstMsgPosition, rsmFirstMsgPositionErr := session.GetChatMessagePosition(toID, messages[0].Id) + if rsmFirstMsgPositionErr == nil && rsmFirstMsgPosition != nil { + index := int(rsmFirstMsgPosition.Count - firstMsgPositionCount) + rs.First.Index = &index + } + } + last := strconv.FormatInt(messages[len(messages)-1].Id, 10) + rs.Last = &last + } + } + log.Debugf("MAM fin: %#v", answer.Payload) +} + func iqAnswerSetError(answer *stanza.IQ, code int) { iqAnswerSetErrorInternal(answer, code, false) } @@ -2086,6 +2637,11 @@ func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) { Type: stanza.ErrorTypeWait, Reason: "internal-server-error", } + case 501: + answer.Error = &stanza.Err{ + Type: stanza.ErrorTypeCancel, + Reason: "feature-not-implemented", + } default: log.Error("Unknown error code, falling back with empty reason") answer.Error = &stanza.Err{ @@ -2251,6 +2807,21 @@ func toToID(to string) (int64, bool, bool) { return toID, true, isGroup } +func parseMessageId(sId string) (int64, bool) { + if sId[0] == 'e' { + idParts := strings.Split(sId[1:], ":") + if len(idParts) >= 1 { + sId = idParts[0] + } + } + id, err := strconv.ParseInt(sId, 10, 64) + if err != nil { + log.Warn(errors.Wrap(err, "Failed to parse message ID!")) + return 0, false + } + return id, true +} + func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload { var base64Photo string if info.Photo != nil { From 2a81da955b3bc36d938581af5ab2e9d3511ed2c3 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 14 Aug 2025 07:36:58 -0400 Subject: [PATCH 195/228] Show mentions with @ for noticeability --- telegram/formatter/formatter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/formatter/formatter.go b/telegram/formatter/formatter.go index 72337bb..2c06ac4 100644 --- a/telegram/formatter/formatter.go +++ b/telegram/formatter/formatter.go @@ -194,7 +194,7 @@ func mentionBraces(entity *client.TextEntity, nickname string) []*insertion { return []*insertion{ &insertion{ Offset: entity.Offset, - Runes: []rune(nickname), + Runes: []rune("@" + nickname), Type: insertionOpening, Replacing: true, }, From 075f327275dd96ee1edb07df4eca9df5bbf9ae3f Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 14 Aug 2025 09:31:29 -0400 Subject: [PATCH 196/228] Log line fix --- xmpp/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 5875367..9809385 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2265,7 +2265,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer iqAnswerSetError(answer, 400) return } - log.Debugf("MAM RSM after: %v", rsmBefore) + log.Debugf("MAM RSM before: %v", rsmBefore) } } if query.ResultSet.Max != nil { From 4fb3886aab5eee59a4d9fec0b3caa25cd773edf8 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 15 Aug 2025 04:31:27 -0400 Subject: [PATCH 197/228] Fix some MAM bugs --- telegram/utils.go | 11 +++++++---- xmpp/handlers.go | 48 +++++++++++++++++------------------------------ 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 004e9ef..8ea29a8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2387,11 +2387,11 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, if limit < 0 { reqFromMessageId = lastMessageId reqOffset = -1 - reqLimit = limit + reqLimit = -limit + 1 } else { reqFromMessageId = fromMessageId - reqOffset = -limit - reqLimit = limit + reqOffset = -limit - 1 + reqLimit = limit + 1 } newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{ @@ -2408,7 +2408,7 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, fromPos := -1 for i, message := range newMessages.Messages { if message.Id == fromMessageId { - fromPos = i + fromPos = i+1 break } } @@ -2420,6 +2420,9 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, complete = true } else { for _, message := range newMessages.Messages { + if message.Id == fromMessageId { + continue + } if lastMessageId != 0 { if message.Id > lastMessageId { break diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 9809385..0d0a6b4 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2368,6 +2368,15 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer iqAnswerSetError(answer, 404) return } + overallyLastMessage, overallyLastMessageErr := session.GetPreviousMessage(toID, beforeId) + if overallyLastMessageErr == nil && overallyLastMessage != nil { + overallyLastMessageId = overallyLastMessage.Id + } + } else { + newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) + if newestMessageErr == nil && newestMessage != nil { + overallyLastMessageId = newestMessage.Id + } } if rsmAfter != 0 { @@ -2392,14 +2401,13 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer } if rsmBefore < beforeId { canBeComplete = false - overallyLastMessageId = beforeId } beforeId = rsmBefore } var lastMessageId int64 - // should be fine even with afterId=0 as it would mean the last as needed - lastMessage, lastMessageErr := session.GetPreviousMessage(toID, afterId) + // should be fine even with beforeId=0 as it would mean the last as needed + lastMessage, lastMessageErr := session.GetPreviousMessage(toID, beforeId) if lastMessageErr == nil && lastMessage != nil { lastMessageId = lastMessage.Id } else { @@ -2411,16 +2419,6 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer messages, newComplete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit) if canBeComplete { complete = newComplete - if !complete { - if lastMessageId != 0 { - overallyLastMessageId = lastMessageId - } else { - newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) - if newestMessageErr == nil && newestMessage != nil { - overallyLastMessageId = newestMessage.Id - } - } - } } } } else { // time limit or no limits at all @@ -2447,9 +2445,15 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer endMsg, endMsgErr := session.FindMessageByTime(toID, endTime) if endMsgErr == nil && endMsg != nil { lastMessageId = endMsg.Id + overallyLastMessageId = lastMessageId } else { beyond = true } + } else { + newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) + if newestMessageErr == nil && newestMessage != nil { + overallyLastMessageId = newestMessage.Id + } } if rsmAfter != 0 { @@ -2486,14 +2490,6 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if newLastMessageErr == nil && newLastMessage != nil { if lastMessageId == 0 || lastMessageId != newLastMessage.Id { canBeComplete = false - if lastMessageId != 0 { - overallyLastMessageId = lastMessageId - } else { - newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) - if newestMessageErr == nil && newestMessage != nil { - overallyLastMessageId = newestMessage.Id - } - } lastMessageId = newLastMessage.Id } } else { @@ -2511,16 +2507,6 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer messages, newComplete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit) if canBeComplete { complete = newComplete - if !complete { - if lastMessageId != 0 { - overallyLastMessageId = lastMessageId - } else { - newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) - if newestMessageErr == nil && newestMessage != nil { - overallyLastMessageId = newestMessage.Id - } - } - } } } } From 17f617e15d5a997ab966b519b5589cda2404e998 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 15 Aug 2025 12:49:18 -0400 Subject: [PATCH 198/228] Always set "complete" attribute for empty history responses --- telegram/utils.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 8ea29a8..34c0914 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2401,9 +2401,6 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, Limit: reqLimit, }) if err == nil { - if len(newMessages.Messages) == 0 { - complete = true - } if limit < 0 { fromPos := -1 for i, message := range newMessages.Messages { @@ -2433,6 +2430,9 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, } messages = append(messages, message) } + if len(messages) == 0 { + complete = true + } } } return From 623af72cdd4d9ef058f73502fabe18685996053f Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 15 Aug 2025 13:12:03 -0400 Subject: [PATCH 199/228] Set "complete" attribute for requests beyond limits for clearly non-RSM reasons --- xmpp/handlers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 0d0a6b4..9811428 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2425,6 +2425,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer // don't allow to fetch far beyond the quota if !endTime.IsZero() && endTime.Before(quotaTs) { beyond = true + complete = true } if (!startTime.IsZero() && startTime.Before(quotaTs)) || startTime.IsZero() { startTime = quotaTs @@ -2448,6 +2449,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer overallyLastMessageId = lastMessageId } else { beyond = true + complete = true } } else { newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0) From 983c0102a44bf6457d6a669032cb446222e815a4 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 16 Aug 2025 09:32:23 -0400 Subject: [PATCH 200/228] Fix MUC member nickname changes --- telegram/handlers.go | 6 +++--- telegram/utils.go | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/telegram/handlers.go b/telegram/handlers.go index 0170d5e..66582a2 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -149,13 +149,13 @@ func (c *Client) updateHandler() { // new user discovered func (c *Client) updateUser(update *client.UpdateUser) { // check if MUC nicknames should be updated - cacheUser, ok := c.cache.GetUser(update.User.Id) - if ok && (cacheUser.FirstName != update.User.FirstName || cacheUser.LastName != update.User.LastName) { + oldCacheUser, ok := c.cache.GetUser(update.User.Id) + c.cache.SetUser(update.User.Id, update.User) + if ok && (oldCacheUser.FirstName != update.User.FirstName || oldCacheUser.LastName != update.User.LastName) { newNickname := c.GetMUCNickname(update.User.Id) c.updateMUCsNickname(update.User.Id, newNickname) } - c.cache.SetUser(update.User.Id, update.User) show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id) go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType)) } diff --git a/telegram/utils.go b/telegram/utils.go index 34c0914..bfdf02d 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -890,6 +890,10 @@ func (c *Client) updateMUCsNickname(memberID int64, newNickname string) { for mucId, state := range c.mucCache { oldOccupant, ok := state.Occupants.Get(memberID) if ok { + c.DelayedStatusesLock.Lock() + delete(c.DelayedStatuses, mucId) + c.DelayedStatusesLock.Unlock() + deleted := state.Occupants.Set(memberID, &MUCOccupant{ Nickname: newNickname, Affiliation: oldOccupant.Affiliation, From 7463a56a7d5d04df88f7bbc85c5b93539c42773f Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 18 Aug 2025 00:22:08 -0400 Subject: [PATCH 201/228] Better MAM fin RSM logging --- xmpp/handlers.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 9811428..b847cac 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2533,6 +2533,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if beyond { count := 0 rs.Count = &count + log.Debugf("MAM: beyond") } else { if len(messages) > 0 { if fromStart { @@ -2575,7 +2576,14 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer rs.Last = &last } } - log.Debugf("MAM fin: %#v", answer.Payload) + if log.GetLevel() == log.DebugLevel { + xmlFin, err := xml.Marshal(answer.Payload) + if err == nil { + log.Debug(string(xmlFin)) + } else { + log.Debugf("MAM fin: %#v %#v", answer.Payload, rs) + } + } } func iqAnswerSetError(answer *stanza.IQ, code int) { From 8ba9d189335b5ca485e002f0a8bb82c13f5db1eb Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 18 Aug 2025 00:22:44 -0400 Subject: [PATCH 202/228] Remove unnecessary -t flag, it is for test dependencies only --- staging.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staging.Dockerfile b/staging.Dockerfile index 95cdea5..ca5a7e6 100644 --- a/staging.Dockerfile +++ b/staging.Dockerfile @@ -27,7 +27,7 @@ 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,rw \ - /bin/bash -c 'go mod tidy; go get -t' + /bin/bash -c 'go mod tidy; go get' FROM cache AS build ARG MAKEOPTS From 3eacc4cec539cb9ce9dcf80485dfc052097595d0 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 18 Aug 2025 00:23:17 -0400 Subject: [PATCH 203/228] Bump go-xmpp version for an RSM first element fix --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5a97cec..3481992 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,6 @@ require ( nhooyr.io/websocket v1.6.5 // indirect ) -replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314 +replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a 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 93efe73..9bd8135 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd h1:+UW+E7JjI 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= +dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a h1:9PPqmhy6HbhhCS5EZzw+sdi4EpWW+LOwnz+/JXTcHjQ= +dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a/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= From 84a6484e86f7b593386061cbf73e1966d0e76318 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 22 Aug 2025 07:26:34 -0400 Subject: [PATCH 204/228] Cut off excessive message in last-page MAM queries more reliably --- telegram/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index bfdf02d..03e1dcf 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2408,7 +2408,7 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, if limit < 0 { fromPos := -1 for i, message := range newMessages.Messages { - if message.Id == fromMessageId { + if message.Id >= fromMessageId { fromPos = i+1 break } From a17b4892fc6a26aa1622b3d962c308a8cf21e44c Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 23 Aug 2025 07:28:08 -0400 Subject: [PATCH 205/228] Fix calculating reply end offset --- telegram/utils.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index 03e1dcf..2fd1bca 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1734,8 +1734,10 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, reply = gwReply var replyStart, replyEnd int + var hadPrefix bool if len(prefix) > 0 { + hadPrefix = true replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) } @@ -1743,7 +1745,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, prefix = append(prefix, replyLine) replyEnd = replyStart + utf8.RuneCountInString(replyLine) - if len(prefix) > 0 { + if hadPrefix { replyEnd += len(messageHeaderSeparator) } From e59ef598a3c4dd5ab25d59dadb4f2e7ee7d6afb2 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 23 Aug 2025 07:51:07 -0400 Subject: [PATCH 206/228] Bump go-xmpp version for another RSM first element fix --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 3481992..2a1c994 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,6 @@ require ( nhooyr.io/websocket v1.6.5 // indirect ) -replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a +replace gosrc.io/xmpp => dev.narayana.im/narayana/go-xmpp v0.0.0-20250823114312-ed4011fc17e4 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 9bd8135..83a3e9e 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314 h1:29/NjOGOU dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a h1:9PPqmhy6HbhhCS5EZzw+sdi4EpWW+LOwnz+/JXTcHjQ= dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +dev.narayana.im/narayana/go-xmpp v0.0.0-20250823114312-ed4011fc17e4 h1:HQT33Zp3iRkbCiijWDo943K//wQgzoMccIP7Vb2uEfY= +dev.narayana.im/narayana/go-xmpp v0.0.0-20250823114312-ed4011fc17e4/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= From b9109d48d5e09e409915b287e41be25fddbbf665 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 24 Aug 2025 08:17:08 -0400 Subject: [PATCH 207/228] That MAM implementation was utterly broken --- telegram/utils.go | 75 +++++++++++++++++++++++++++++++---------------- xmpp/handlers.go | 61 +++++++++++++++++++++++++------------- 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index 2fd1bca..b127ae8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2375,7 +2375,7 @@ func (c *Client) getNLastMessages(chatID int64, limit *MessageLimit) ([]*client. } // GetMessagesBetween lazily fetches message history between given ids (from exclusive, last inclusive), also calculating completeness flag; negative limit means messages from the end -func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, limit int32) (messages []*client.Message, complete bool, err error) { +func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, limit int32, reverse bool) (messages []*client.Message, complete bool, err error) { log.WithFields(log.Fields{ "chat_id": chatID, "from": fromMessageId, @@ -2399,6 +2399,11 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, reqOffset = -limit - 1 reqLimit = limit + 1 } + log.WithFields(log.Fields{ + "from": reqFromMessageId, + "offset": reqOffset, + "limit": reqLimit, + }).Debug("calculated history request") newMessages, err = c.client.GetChatHistory(&client.GetChatHistoryRequest{ ChatId: chatID, @@ -2407,12 +2412,28 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, Limit: reqLimit, }) if err == nil { - if limit < 0 { + log.Debugf("pre-fetched %v messages, cutting", len(newMessages.Messages)) + if limit > 0 { + complete = true fromPos := -1 - for i, message := range newMessages.Messages { - if message.Id >= fromMessageId { - fromPos = i+1 - break + if lastMessageId != 0 { + for i, message := range newMessages.Messages { + if message.Id < lastMessageId { + complete = false + break + } else if message.Id == lastMessageId { + fromPos = i + break + } + } + } else { + if len(newMessages.Messages) > 0 { + lastMsg, lastMsgErr := c.GetPreviousMessage(chatID, 0) + if lastMsgErr == nil && lastMsg != nil { + if lastMsg.Id != newMessages.Messages[0].Id { + complete = false + } + } } } if fromPos > -1 { @@ -2420,27 +2441,19 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, } else { messages = newMessages.Messages } - complete = true } else { + complete = true for _, message := range newMessages.Messages { - if message.Id == fromMessageId { - continue - } - if lastMessageId != 0 { - if message.Id > lastMessageId { - break - complete = true - } else if message.Id == lastMessageId { - complete = true - } + if message.Id <= fromMessageId { + break } messages = append(messages, message) } - if len(messages) == 0 { - complete = true - } } } + if !reverse { + ReverseMessagesSlice(messages) + } return } @@ -3394,12 +3407,22 @@ func GetErrorCode(err error) (int32, bool) { } // ChronologicallySortMessages is… self-explanatory (Achtung: destructive) -func ChronologicallySortMessages(messages []*client.Message) []*client.Message { - sort.Slice(messages, func(i int, j int) bool { - msg1 := messages[i] - msg2 := messages[j] - return msg1.Date < msg2.Date - }) +func ChronologicallySortMessages(messages []*client.Message, reverse bool) []*client.Message { + var sortFunc func(int, int) bool + if reverse { + sortFunc = func(i int, j int) bool { + msg1 := messages[i] + msg2 := messages[j] + return msg1.Date > msg2.Date + } + } else { + sortFunc = func(i int, j int) bool { + msg1 := messages[i] + msg2 := messages[j] + return msg1.Date < msg2.Date + } + } + sort.Slice(messages, sortFunc) return messages } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index b847cac..90aa9d6 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2259,6 +2259,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer before := *query.ResultSet.Before if before == "" { rsmLastPage = true + log.Debugf("MAM RSM last page") } else { rsmBefore, ok = parseMessageId(*query.ResultSet.Before) if !ok { @@ -2303,11 +2304,12 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer ((beforeId != 0 || afterId != 0) && (!startTime.IsZero() || !endTime.IsZero() || ids != nil)) || (ids != nil && (!startTime.IsZero() || !endTime.IsZero() || beforeId != 0 || afterId != 0)) { iqAnswerSetError(answer, 501) + log.Debugf("MAM: incompatible parameters") return } // dummy call to circumvent unexported type - messages, _, err := session.GetMessagesBetween(toID, 0, 0, 0) + messages, _, err := session.GetMessagesBetween(toID, 0, 0, 0, false) quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold)) var beyond, complete bool @@ -2315,25 +2317,30 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer fromStart := true var overallyFirstMessageId, overallyLastMessageId int64 + reverse := query.FlipPage != nil + if ids != nil { for _, sId := range ids { id, ok := parseMessageId(sId) if !ok { iqAnswerSetError(answer, 400) + log.Debugf("MAM: bogus id") return } msg, err := session.GetMessage(toID, id) if err != nil { iqAnswerSetError(answer, 404) + log.Debugf("MAM: unknown id") return } messages = append(messages, msg) } - messages = telegram.ChronologicallySortMessages(messages) + messages = telegram.ChronologicallySortMessages(messages, reverse) complete = true } else if beforeId != 0 || afterId != 0 { if (beforeId != 0 && afterId != 0) && beforeId > afterId { iqAnswerSetError(answer, 400) + log.Debugf("MAM: before > after") return } @@ -2347,6 +2354,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer } } else { iqAnswerSetError(answer, 404) + log.Debugf("MAM: unknown after") return } } else { @@ -2366,6 +2374,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer beforeMessage, beforeMessageErr := session.GetMessage(toID, beforeId) if beforeMessageErr != nil || beforeMessage == nil { iqAnswerSetError(answer, 404) + log.Debugf("MAM: unknown before") return } overallyLastMessage, overallyLastMessageErr := session.GetPreviousMessage(toID, beforeId) @@ -2382,6 +2391,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if rsmAfter != 0 { if rsmAfter < afterId || (beforeId != 0 && rsmAfter > beforeId) { iqAnswerSetError(answer, 400) + log.Debugf("MAM: unknown RSM after") return } if rsmAfter != afterId { @@ -2397,6 +2407,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if rsmBefore != 0 { if rsmBefore < afterId || (beforeId != 0 && rsmBefore > beforeId) { iqAnswerSetError(answer, 400) + log.Debugf("MAM: unknown RSM before") return } if rsmBefore < beforeId { @@ -2416,7 +2427,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if !beyond { var newComplete bool - messages, newComplete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit) + messages, newComplete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit, reverse) if canBeComplete { complete = newComplete } @@ -2461,12 +2472,13 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if rsmAfter != 0 { rsmAfterMessage, rsmAfterMessageErr := session.GetMessage(toID, rsmAfter) if rsmAfterMessageErr == nil && rsmAfterMessage != nil { - if int64(rsmAfterMessage.Date) < startTime.Unix() || (!endTime.IsZero() && int64(rsmAfterMessage.Date) > endTime.Unix()) { + if !endTime.IsZero() && int64(rsmAfterMessage.Date) > endTime.Unix() { iqAnswerSetError(answer, 400) + log.Debugf("MAM: RSM after out of range") return } - if rsmAfterMessage.Id > fromMessageId { + if rsmAfterMessage.Id > fromMessageId && int64(rsmAfterMessage.Date) >= startTime.Unix() { fromStart = false overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, fromMessageId) if overallyFirstMessageErr == nil && overallyFirstMessage != nil { @@ -2476,6 +2488,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer fromMessageId = rsmAfterMessage.Id } else { iqAnswerSetError(answer, 404) + log.Debugf("MAM: unknown RSM after") return } } @@ -2483,30 +2496,34 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if rsmBefore != 0 { rsmBeforeMessage, rsmBeforeMessageErr := session.GetMessage(toID, rsmBefore) if rsmBeforeMessageErr == nil && rsmBeforeMessage != nil { - if int64(rsmBeforeMessage.Date) < startTime.Unix() || (!endTime.IsZero() && int64(rsmBeforeMessage.Date) > endTime.Unix()) { + if int64(rsmBeforeMessage.Date) < startTime.Unix() { iqAnswerSetError(answer, 400) + log.Debugf("MAM: RSM before out of range") return } - newLastMessage, newLastMessageErr := session.GetPreviousMessage(toID, rsmBeforeMessage.Id) - if newLastMessageErr == nil && newLastMessage != nil { - if lastMessageId == 0 || lastMessageId != newLastMessage.Id { - canBeComplete = false - lastMessageId = newLastMessage.Id + if endTime.IsZero() || int64(rsmBeforeMessage.Date) <= endTime.Unix() { + newLastMessage, newLastMessageErr := session.GetPreviousMessage(toID, rsmBeforeMessage.Id) + if newLastMessageErr == nil && newLastMessage != nil { + if lastMessageId == 0 || lastMessageId != newLastMessage.Id { + canBeComplete = false + lastMessageId = newLastMessage.Id + } + } else { + // nothing?.. not complete, just empty + beyond = true } - } else { - // nothing?.. not complete, just empty - beyond = true } } else { iqAnswerSetError(answer, 404) + log.Debugf("MAM: unknown RSM before") return } } if !beyond { // yes🗿, twice var newComplete bool - messages, newComplete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit) + messages, newComplete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit, reverse) if canBeComplete { complete = newComplete } @@ -2514,10 +2531,6 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer } } - if query.FlipPage != nil { - telegram.ReverseMessagesSlice(messages) - } - log.Debugf("obtained %v messages", len(messages)) for _, message := range messages { session.SendDelayedMUCMessage(toID, message, iq.From, query.QueryId) @@ -2555,8 +2568,14 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if lastMsgPositionErr == nil && lastMsgPosition != nil { lastMsgPositionCount = lastMsgPosition.Count } + log.WithFields(log.Fields{ + "overallyFirstMessageId": overallyFirstMessageId, + "overallyLastMessageId": overallyLastMessageId, + "firstMsgPositionCount": firstMsgPositionCount, + "lastMsgPositionCount": lastMsgPositionCount, + }).Debug("RSM count") if firstMsgPositionCount != 0 && lastMsgPositionCount != 0 { - count := int(lastMsgPositionCount - firstMsgPositionCount + 1) + count := int(firstMsgPositionCount - lastMsgPositionCount + 1) rs.Count = &count } } @@ -2568,7 +2587,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer if firstMsgPositionCount != 0 { rsmFirstMsgPosition, rsmFirstMsgPositionErr := session.GetChatMessagePosition(toID, messages[0].Id) if rsmFirstMsgPositionErr == nil && rsmFirstMsgPosition != nil { - index := int(rsmFirstMsgPosition.Count - firstMsgPositionCount) + index := int(firstMsgPositionCount - rsmFirstMsgPosition.Count) rs.First.Index = &index } } From 0f10dbff8c7637652e440a0df94ff004ebdb78b1 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 25 Aug 2025 09:27:31 -0400 Subject: [PATCH 208/228] Take flip-page into account when calculating MAM RSM count/index --- xmpp/handlers.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 90aa9d6..a6560e3 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2549,11 +2549,21 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer log.Debugf("MAM: beyond") } else { if len(messages) > 0 { + firstMsgId := messages[0].Id + lastMsgId := messages[len(messages)-1].Id if fromStart { - overallyFirstMessageId = messages[0].Id + if reverse { + overallyFirstMessageId = lastMsgId + } else { + overallyFirstMessageId = firstMsgId + } } if complete { - overallyLastMessageId = messages[len(messages)-1].Id + if reverse { + overallyLastMessageId = firstMsgId + } else { + overallyLastMessageId = lastMsgId + } } } @@ -2581,17 +2591,22 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer } if len(messages) > 0 { + firstMsgId := messages[0].Id + lastMsgId := messages[len(messages)-1].Id + if reverse { + firstMsgId, lastMsgId = lastMsgId, firstMsgId + } rs.First = &stanza.First{ - Content: strconv.FormatInt(messages[0].Id, 10), + Content: strconv.FormatInt(firstMsgId, 10), } if firstMsgPositionCount != 0 { - rsmFirstMsgPosition, rsmFirstMsgPositionErr := session.GetChatMessagePosition(toID, messages[0].Id) + rsmFirstMsgPosition, rsmFirstMsgPositionErr := session.GetChatMessagePosition(toID, firstMsgId) if rsmFirstMsgPositionErr == nil && rsmFirstMsgPosition != nil { index := int(firstMsgPositionCount - rsmFirstMsgPosition.Count) rs.First.Index = &index } } - last := strconv.FormatInt(messages[len(messages)-1].Id, 10) + last := strconv.FormatInt(lastMsgId, 10) rs.Last = &last } } From c4341433d60920db98fc8a0f6fc76ab28f3cebc8 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 25 Aug 2025 10:51:26 -0400 Subject: [PATCH 209/228] Support legacy MAM versions --- xmpp/extensions/extensions.go | 177 ++++++++++++++++++++++++++++++++++ xmpp/gateway/gateway.go | 47 +++++++-- xmpp/handlers.go | 121 +++++++++++++++++------ 3 files changed, 305 insertions(+), 40 deletions(-) diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index ba03643..0414fc8 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -376,6 +376,30 @@ type MAM2Query struct { FlipPage *FlipPage `xml:"flip-page"` } +// MAM1Query is from XEP-0313 +type MAM1Query struct { + XMLName xml.Name `xml:"urn:xmpp:mam:1 query"` + Form *stanza.Form `xml:"jabber:x:data x"` + QueryId string `xml:"queryid,attr,omitempty"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + +// MAM0Query is from XEP-0313 +type MAM0Query struct { + XMLName xml.Name `xml:"urn:xmpp:mam:0 query"` + Form *stanza.Form `xml:"jabber:x:data x"` + QueryId string `xml:"queryid,attr,omitempty"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + +type MAMQuery interface { + Namespace() string + GetForm() *stanza.Form + GetQueryId() string + GetSet() *stanza.ResultSet + GetFlipPage() *FlipPage +} + // FlipPage is an extended element from XEP-0313 type FlipPage struct { XMLName xml.Name `xml:"flip-page"` @@ -396,6 +420,22 @@ type MAM2MessageResult struct { Id string `xml:"id,attr,omitempty"` } +// MAM1MessageResult is from XEP-0313 +type MAM1MessageResult struct { + XMLName xml.Name `xml:"urn:xmpp:mam:1 result"` + Forwarded *ForwardedMessage `xml:"urn:xmpp:forward:0 forwarded,omitempty"` + QueryId string `xml:"queryid,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` +} + +// MAM0MessageResult is from XEP-0313 +type MAM0MessageResult struct { + XMLName xml.Name `xml:"urn:xmpp:mam:0 result"` + Forwarded *ForwardedMessage `xml:"urn:xmpp:forward:0 forwarded,omitempty"` + QueryId string `xml:"queryid,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` +} + // MAM2Fin is from XEP-0313 type MAM2Fin struct { XMLName xml.Name `xml:"urn:xmpp:mam:2 fin"` @@ -404,6 +444,22 @@ type MAM2Fin struct { Stable bool `xml:"stable,attr"` } +// MAM1Fin is from XEP-0313 +type MAM1Fin struct { + XMLName xml.Name `xml:"urn:xmpp:mam:1 fin"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` + Complete bool `xml:"complete,attr,omitempty"` + Stable bool `xml:"stable,attr"` +} + +// MAM0Fin is from XEP-0313 +type MAM0Fin struct { + XMLName xml.Name `xml:"urn:xmpp:mam:0 fin"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` + Complete bool `xml:"complete,attr,omitempty"` + Stable bool `xml:"stable,attr"` +} + // MAM2Metadata is from XEP-0313 type MAM2Metadata struct { XMLName xml.Name `xml:"urn:xmpp:mam:2 metadata"` @@ -536,11 +592,76 @@ func (c MAM2Query) Namespace() string { return c.XMLName.Space } +// GetForm obtains the query form +func (c MAM2Query) GetForm() *stanza.Form { + return c.Form +} + +// GetQueryId obtains the query id +func (c MAM2Query) GetQueryId() string { + return c.QueryId +} + // GetSet getsets! func (c MAM2Query) GetSet() *stanza.ResultSet { return c.ResultSet } +// GetFlipPage obtains the flip-page element +func (c MAM2Query) GetFlipPage() *FlipPage { + return c.FlipPage +} + +// Namespace is a namespace! +func (c MAM1Query) Namespace() string { + return c.XMLName.Space +} + +// GetForm obtains the query form +func (c MAM1Query) GetForm() *stanza.Form { + return c.Form +} + +// GetQueryId obtains the query id +func (c MAM1Query) GetQueryId() string { + return c.QueryId +} + +// GetSet getsets! +func (c MAM1Query) GetSet() *stanza.ResultSet { + return c.ResultSet +} + +// GetFlipPage is a stub as it's not supported in this MAM version +func (c MAM1Query) GetFlipPage() *FlipPage { + return nil +} + +// Namespace is a namespace! +func (c MAM0Query) Namespace() string { + return c.XMLName.Space +} + +// GetForm obtains the query form +func (c MAM0Query) GetForm() *stanza.Form { + return c.Form +} + +// GetQueryId obtains the query id +func (c MAM0Query) GetQueryId() string { + return c.QueryId +} + +// GetSet getsets! +func (c MAM0Query) GetSet() *stanza.ResultSet { + return c.ResultSet +} + +// GetFlipPage is a stub as it's not supported in this MAM version +func (c MAM0Query) GetFlipPage() *FlipPage { + return nil +} + // Namespace is a namespace! func (c MAM2Fin) Namespace() string { return c.XMLName.Space @@ -551,6 +672,26 @@ func (c MAM2Fin) GetSet() *stanza.ResultSet { return c.ResultSet } +// Namespace is a namespace! +func (c MAM1Fin) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c MAM1Fin) GetSet() *stanza.ResultSet { + return c.ResultSet +} + +// Namespace is a namespace! +func (c MAM0Fin) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c MAM0Fin) GetSet() *stanza.ResultSet { + return c.ResultSet +} + // Namespace is a namespace! func (c MAM2Metadata) Namespace() string { return c.XMLName.Space @@ -701,18 +842,54 @@ func init() { "query", }, MAM2Query{}) + // MAM1 query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:mam:1", + "query", + }, MAM1Query{}) + + // MAM0 query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:mam:0", + "query", + }, MAM0Query{}) + // MAM2 message result stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ "urn:xmpp:mam:2", "result", }, MAM2MessageResult{}) + // MAM1 message result + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:mam:1", + "result", + }, MAM1MessageResult{}) + + // MAM0 message result + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:mam:0", + "result", + }, MAM0MessageResult{}) + // MAM2 fin stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ "urn:xmpp:mam:2", "fin", }, MAM2Fin{}) + // MAM1 fin + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:mam:1", + "fin", + }, MAM1Fin{}) + + // MAM0 fin + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:mam:0", + "fin", + }, MAM0Fin{}) + // MAM2 metadata stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ "urn:xmpp:mam:2", diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 4657a2f..f8e6533 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -50,6 +50,10 @@ const NodeAvatarMetadataNotify string = NodeAvatarMetadata + "+notify" const NodeAvatarData string = "urn:xmpp:avatar:data" const NSCommand string = "http://jabber.org/protocol/commands" +const NS_MAM2 = "urn:xmpp:mam:2" +const NS_MAM1 = "urn:xmpp:mam:1" +const NS_MAM0 = "urn:xmpp:mam:0" + // Queue stores presences to send later var Queue = make(map[string]*stanza.Presence) var QueueLock = sync.Mutex{} @@ -437,23 +441,46 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component } sendMessage(&privilegeMessage, component) } else if mamQueryId != "" { + mneVpadluProbrasyvatJoshParametryPoraRefaktorit := strings.Split(mamQueryId, " ") + ns := mneVpadluProbrasyvatJoshParametryPoraRefaktorit[0] + mamQueryId = mneVpadluProbrasyvatJoshParametryPoraRefaktorit[1] delay := extensions.NewMessageDelay(timestamp, "") + + forwarded := extensions.ForwardedMessage{ + Delay: &delay, + Message: &message, + } + + var ext stanza.MsgExtension + + switch ns { + case NS_MAM2: + ext = extensions.MAM2MessageResult{ + Id: stanzaId, + QueryId: mamQueryId, + Forwarded: &forwarded, + } + case NS_MAM1: + ext = extensions.MAM1MessageResult{ + Id: stanzaId, + QueryId: mamQueryId, + Forwarded: &forwarded, + } + case NS_MAM0: + ext = extensions.MAM0MessageResult{ + Id: stanzaId, + QueryId: mamQueryId, + Forwarded: &forwarded, + } + } + mamMessage := stanza.Message{ Attrs: stanza.Attrs{ From: mucJID, To: to, Type: messageType, }, - Extensions: []stanza.MsgExtension{ - extensions.MAM2MessageResult{ - Id: stanzaId, - QueryId: mamQueryId, - Forwarded: &extensions.ForwardedMessage{ - Delay: &delay, - Message: &message, - }, - }, - }, + Extensions: []stanza.MsgExtension{ext}, } sendMessage(&mamMessage, component) } else { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a6560e3..057aeff 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -85,7 +85,17 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { } queryMAM2, ok := iq.Payload.(*extensions.MAM2Query) if ok { - go handleGetQueryMAM2(s, iq, queryMAM2) + go handleGetQueryMAM(s, iq, queryMAM2) + return + } + queryMAM1, ok := iq.Payload.(*extensions.MAM1Query) + if ok { + go handleGetQueryMAM(s, iq, queryMAM1) + return + } + queryMAM0, ok := iq.Payload.(*extensions.MAM0Query) + if ok { + go handleGetQueryMAM(s, iq, queryMAM0) return } _, ok = iq.Payload.(*extensions.MAM2Metadata) @@ -116,7 +126,17 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { } queryMAM2, ok := iq.Payload.(*extensions.MAM2Query) if ok { - go handleSetQueryMAM2(s, iq, queryMAM2) + go handleSetQueryMAM(s, iq, queryMAM2) + return + } + queryMAM1, ok := iq.Payload.(*extensions.MAM1Query) + if ok { + go handleSetQueryMAM(s, iq, queryMAM1) + return + } + queryMAM0, ok := iq.Payload.(*extensions.MAM0Query) + if ok { + go handleSetQueryMAM(s, iq, queryMAM0) return } } else if iq.Type == stanza.IQTypeResult { @@ -976,7 +996,9 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { "muc_unsecured", "http://jabber.org/protocol/muc#stable_id", "jabber:iq:register", - "urn:xmpp:mam:2", + gateway.NS_MAM0, + gateway.NS_MAM1, + gateway.NS_MAM2, "urn:xmpp:mam:2#extended", "urn:xmpp:sid:0", "vcard-temp", @@ -1382,23 +1404,33 @@ func handleGetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ) { } } -func handleGetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Query) { +func handleGetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) { component, answer, ok := iqResultStub(s, iq) if !ok { return } defer gateway.ResumableSend(component, answer) - payload := &extensions.MAM2Query{} - answer.Payload = payload + payload2 := &extensions.MAM2Query{} + payload1 := &extensions.MAM1Query{} + payload0 := &extensions.MAM0Query{} + ns := query.Namespace() + switch ns { + case gateway.NS_MAM2: + answer.Payload = payload2 + case gateway.NS_MAM1: + answer.Payload = payload1 + case gateway.NS_MAM0: + answer.Payload = payload0 + } - payload.Form = &stanza.Form{ + form := stanza.Form{ Type: stanza.FormTypeForm, Fields: []*stanza.Field{ &stanza.Field{ Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, - ValuesList: []string{"urn:xmpp:mam:2"}, + ValuesList: []string{ns}, }, &stanza.Field{ Var: "with", @@ -1426,6 +1458,9 @@ func handleGetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer }, }, } + payload2.Form = &form + payload1.Form = &form + payload0.Form = &form log.Debugf("MAM info request: %#v", query) } @@ -2154,7 +2189,7 @@ func handleSetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ, query *extensions.Quer } -func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Query) { +func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) { component, answer, ok := iqResultStub(s, iq) if !ok { return @@ -2194,9 +2229,13 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer var rsmLimit int32 var justCount bool - log.Debugf("MAM query to %v: %#v %#v %#v", toID, query, query.Form, query.ResultSet) - if query.Form != nil && query.Form.Type == stanza.FormTypeSubmit { - for _, field := range query.Form.Fields { + form := query.GetForm() + queryRs := query.GetSet() + ns := query.Namespace() + + log.Debugf("MAM query to %v: %#v %#v %#v", toID, query, form, queryRs) + if form != nil && form.Type == stanza.FormTypeSubmit { + for _, field := range form.Fields { if len(field.ValuesList) < 1 { iqAnswerSetError(answer, 400) return @@ -2207,7 +2246,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer switch field.Var { case "FORM_TYPE": - if value != "urn:xmpp:mam:2" { + if value != ns { iqAnswerSetError(answer, 400) return } @@ -2246,22 +2285,22 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer } } - if query.ResultSet != nil { - if query.ResultSet.After != nil { - rsmAfter, ok = parseMessageId(*query.ResultSet.After) + if queryRs != nil { + if queryRs.After != nil { + rsmAfter, ok = parseMessageId(*queryRs.After) if !ok { iqAnswerSetError(answer, 400) return } log.Debugf("MAM RSM after: %v", rsmAfter) } - if query.ResultSet.Before != nil { - before := *query.ResultSet.Before + if queryRs.Before != nil { + before := *queryRs.Before if before == "" { rsmLastPage = true log.Debugf("MAM RSM last page") } else { - rsmBefore, ok = parseMessageId(*query.ResultSet.Before) + rsmBefore, ok = parseMessageId(*queryRs.Before) if !ok { iqAnswerSetError(answer, 400) return @@ -2269,23 +2308,23 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer log.Debugf("MAM RSM before: %v", rsmBefore) } } - if query.ResultSet.Max != nil { - rsmLimit = int32(*query.ResultSet.Max) + if queryRs.Max != nil { + rsmLimit = int32(*queryRs.Max) if rsmLimit == 0 { justCount = true } log.Debugf("MAM RSM max: %v", rsmLimit) } - if query.ResultSet.First != nil { + if queryRs.First != nil { iqAnswerSetError(answer, 400) return } - if query.ResultSet.Index != nil { + if queryRs.Index != nil { iqAnswerSetError(answer, 501) return } - if query.ResultSet.Last != nil { + if queryRs.Last != nil { iqAnswerSetError(answer, 400) return } @@ -2317,7 +2356,7 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer fromStart := true var overallyFirstMessageId, overallyLastMessageId int64 - reverse := query.FlipPage != nil + reverse := query.GetFlipPage() != nil if ids != nil { for _, sId := range ids { @@ -2532,15 +2571,37 @@ func handleSetQueryMAM2(s xmpp.Sender, iq *stanza.IQ, query *extensions.MAM2Quer } log.Debugf("obtained %v messages", len(messages)) + // ty zhe lopnesh, detochka + queryId := ns + " " + query.GetQueryId() for _, message := range messages { - session.SendDelayedMUCMessage(toID, message, iq.From, query.QueryId) + session.SendDelayedMUCMessage(toID, message, iq.From, queryId) } rs := stanza.ResultSet{} - answer.Payload = &extensions.MAM2Fin{ - ResultSet: &rs, - Complete: complete, - Stable: false, + switch ns { + case gateway.NS_MAM2: + answer.Payload = &extensions.MAM2Fin{ + ResultSet: &rs, + Complete: complete, + Stable: false, + } + case gateway.NS_MAM1: + answer.Payload = &extensions.MAM1Fin{ + ResultSet: &rs, + Complete: complete, + Stable: false, + } + case gateway.NS_MAM0: + answer.Payload = &extensions.MAM0Fin{ + ResultSet: &rs, + Complete: complete, + Stable: false, + } + } + + if answer.Payload == nil { + log.Error("Unknown MAM version") + return } if beyond { From 6e0da6ba3a808602b3de07673bf70a8c81addb8a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 26 Aug 2025 11:12:55 -0400 Subject: [PATCH 210/228] Make the child message of forward belong to jabber:client namespace --- xmpp/extensions/extensions.go | 2 +- xmpp/gateway/gateway.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 0414fc8..dcf8b3f 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -408,7 +408,7 @@ type FlipPage struct { // ForwardedMessage is from XEP-0297 (go-xmpp lacks Delay) type ForwardedMessage struct { XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"` - Message *stanza.Message `xml:"message"` + Message *ClientMessage `xml:"jabber:client message"` Delay *MessageDelay `xml:"urn:xmpp:delay delay,omitempty"` } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index f8e6533..863950c 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -446,9 +446,18 @@ func sendMessageWrapper(to, from, body, subject, errorText, id string, component mamQueryId = mneVpadluProbrasyvatJoshParametryPoraRefaktorit[1] delay := extensions.NewMessageDelay(timestamp, "") + clientMessage := extensions.ClientMessage{ + Attrs: message.Attrs, + Subject: message.Subject, + Body: message.Body, + Thread: message.Thread, + Error: message.Error, + Extensions: message.Extensions, + } + forwarded := extensions.ForwardedMessage{ Delay: &delay, - Message: &message, + Message: &clientMessage, } var ext stanza.MsgExtension From bfb1786a42bb21e3c24504378a7f646fddea2d7f Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 26 Aug 2025 13:10:20 -0400 Subject: [PATCH 211/228] Additional request for loading last messages --- telegram/utils.go | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index b127ae8..d6f8a20 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2412,12 +2412,28 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, Limit: reqLimit, }) if err == nil { - log.Debugf("pre-fetched %v messages, cutting", len(newMessages.Messages)) + fetchedMessages := newMessages.Messages + log.Debugf("pre-fetched %v messages, cutting", len(fetchedMessages)) + + // it's not quite good to yield just one last message with the complete flag, + // as clients won't fetch more, give it one more chance + if limit < -1 && len(fetchedMessages) == 1 { + additionalMessages, err := c.client.GetChatHistory(&client.GetChatHistoryRequest{ + ChatId: chatID, + FromMessageId: fetchedMessages[0].Id, + Limit: reqLimit-1, + }) + if err == nil { + log.Debugf("fetched %v more messages", len(additionalMessages.Messages)) + fetchedMessages = append(fetchedMessages, additionalMessages.Messages...) + } + } + if limit > 0 { complete = true fromPos := -1 if lastMessageId != 0 { - for i, message := range newMessages.Messages { + for i, message := range fetchedMessages { if message.Id < lastMessageId { complete = false break @@ -2427,32 +2443,33 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, } } } else { - if len(newMessages.Messages) > 0 { + if len(fetchedMessages) > 0 { lastMsg, lastMsgErr := c.GetPreviousMessage(chatID, 0) if lastMsgErr == nil && lastMsg != nil { - if lastMsg.Id != newMessages.Messages[0].Id { + if lastMsg.Id != fetchedMessages[0].Id { complete = false } } } } if fromPos > -1 { - messages = newMessages.Messages[fromPos:] + messages = fetchedMessages[fromPos:] } else { - messages = newMessages.Messages + messages = fetchedMessages } } else { complete = true - for _, message := range newMessages.Messages { + for _, message := range fetchedMessages { if message.Id <= fromMessageId { break } messages = append(messages, message) } } - } - if !reverse { - ReverseMessagesSlice(messages) + + if !reverse { + ReverseMessagesSlice(messages) + } } return } From f2867a7fc3ee28741cd45facfa87309c342d43e8 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 26 Aug 2025 15:40:11 -0400 Subject: [PATCH 212/228] Return last page for any queries that specify RSM before --- xmpp/handlers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 057aeff..194c861 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2330,6 +2330,10 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) } } + if rsmBefore != 0 && rsmAfter == 0 { + rsmLastPage = true + } + if rsmLimit == 0 && !justCount { rsmLimit = 100 } @@ -2353,7 +2357,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold)) var beyond, complete bool canBeComplete := true - fromStart := true + fromStart := !rsmLastPage var overallyFirstMessageId, overallyLastMessageId int64 reverse := query.GetFlipPage() != nil From 7c9ea42f991f01df1d5e27f2605cd306f5eb1f18 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 27 Aug 2025 12:32:12 -0400 Subject: [PATCH 213/228] Fix id clash for OOB caption messages --- telegram/utils.go | 5 ++++- xmpp/handlers.go | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/telegram/utils.go b/telegram/utils.go index d6f8a20..fa48164 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1984,13 +1984,16 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id // forward message to XMPP var sId string var stanzaId string + var auxId string strId := strconv.FormatInt(message.Id, 10) if id == "" { sId = strId stanzaId = strId + auxId = "c"+strId } else { sId = id stanzaId = strId + auxId = "" } var from string @@ -2031,7 +2034,7 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id for _, jid := range jids { gateway.SendMessageWithOOB(jid, from, text, sId, c.xmpp, reply, timestamp, oob, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId, mamQueryId, mucJID, mucUserItem) if auxText != "" { - gateway.SendMessage(jid, from, auxText, sId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, stanzaId, mamQueryId, mucJID, mucUserItem) + gateway.SendMessage(jid, from, auxText, auxId, c.xmpp, reply, timestamp, "", isCarbon, isGroupchat, c.Session.Receipts, originalFrom, auxId, mamQueryId, mucJID, mucUserItem) } } c.UpdateLastChatMessageId(chatId, sId) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 194c861..38266da 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2908,6 +2908,8 @@ func parseMessageId(sId string) (int64, bool) { if len(idParts) >= 1 { sId = idParts[0] } + } else if sId[0] == 'c' { + sId = sId[1:] } id, err := strconv.ParseInt(sId, 10, 64) if err != nil { From b33cc3c75cda5d42c294deaaf582f691efb4288a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 28 Aug 2025 09:49:14 -0400 Subject: [PATCH 214/228] Disallow non-BMP characters in occupant nicknames --- telegram/utils.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/telegram/utils.go b/telegram/utils.go index fa48164..7bdf588 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -70,6 +70,8 @@ var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" const messageHeaderSeparator string = " | " // no hrunicode allowed here yet +var bmpCeil = rune(0x0000ffff) + // ChatType is an enum of chat types, roughly corresponding to TDLib's one but better type ChatType int @@ -859,6 +861,15 @@ func (c *Client) GetMUCNickname(chatID int64) string { } fc := c.FormatContact(chatID) rp, err := gateway.ResourcePrep(fc) + if err == nil { + // additionally check for non-BMP characters + for _, r := range rp { + if r > bmpCeil { + err = errors.New("Non-BMP character") + break + } + } + } if err != nil { log.Warnf("Resourceprep for %v failed, falling back to chat ID", fc) From 326c94973acd10aef681af0f53fec3233f48c269 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 13 Sep 2025 17:01:09 -0400 Subject: [PATCH 215/228] Treat incoming probe presence same as available only first time after transport restart --- Makefile | 2 +- telegabber.go | 2 +- xmpp/handlers.go | 43 ++++++++++++++++++++++++++++--------------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index dca71a3..254c3b1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.4" +VERSION := "v1.12.5" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index c68ade6..df34b58 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.4" +var version string = "1.12.5" var commit string var sm *goxmpp.StreamManager diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 3c3b370..8e5da5c 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -7,6 +7,7 @@ import ( "sort" "strconv" "strings" + "sync" "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/telegram" @@ -25,6 +26,10 @@ const ( TypeVCard4 ) +// kludge for client resuming after the transport restart (by full jids) +var probeFired = make(map[string]bool) +var probeFiredLock = sync.Mutex{} + func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) } @@ -410,23 +415,31 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { if err != nil { log.Error(errors.Wrap(err, "TDlib connection failure")) } else { - for status := range session.StatusesRange() { - show, description, typ := status.Destruct() - newArgs := []args.V{ - gateway.SPImmed(false), + var probeFiredForFrom bool + probeFiredLock.Lock() + _, probeFiredForFrom = probeFired[p.From] + probeFired[p.From] = true + probeFiredLock.Unlock() + + if p.Type != "probe" || !probeFiredForFrom { + for status := range session.StatusesRange() { + show, description, typ := status.Destruct() + newArgs := []args.V{ + gateway.SPImmed(false), + } + if typ != "" { + newArgs = append(newArgs, gateway.SPType(typ)) + } + go session.ProcessStatusUpdate( + status.ID, + description, + show, + newArgs..., + ) } - if typ != "" { - newArgs = append(newArgs, gateway.SPType(typ)) - } - go session.ProcessStatusUpdate( - status.ID, - description, - show, - newArgs..., - ) + probeClientFeatures(p.From, component) + session.UpdateChatNicknames() } - probeClientFeatures(p.From, component) - session.UpdateChatNicknames() } }() } From 140cf7fa4aabff05a27b66ad13eba53e16b09837 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 16 Sep 2025 08:15:42 -0400 Subject: [PATCH 216/228] Keep two chat caches to avoid unnecessary presences --- Makefile | 2 +- telegabber.go | 2 +- telegram/cache/cache.go | 43 +++++++++++++++++++++++++++----- telegram/commands.go | 24 +++++++++--------- telegram/connect.go | 2 +- telegram/handlers.go | 14 +++++------ telegram/utils.go | 54 ++++++++++++++++++++--------------------- xmpp/component.go | 1 + xmpp/handlers.go | 13 +++++----- 9 files changed, 94 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index 254c3b1..3182744 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.5" +VERSION := "v1.12.6" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index df34b58..cbc58b3 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.5" +var version string = "1.12.6" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/cache/cache.go b/telegram/cache/cache.go index 6847d3e..1261497 100644 --- a/telegram/cache/cache.go +++ b/telegram/cache/cache.go @@ -16,7 +16,8 @@ type Status struct { // Cache allows operating the chats and users cache in // a thread-safe manner type Cache struct { - chats map[int64]*client.Chat + ownChats map[int64]*client.Chat + auxChats map[int64]*client.Chat users map[int64]*client.User statuses map[int64]*Status chatsLock sync.Mutex @@ -27,7 +28,8 @@ type Cache struct { // NewCache initializes a cache func NewCache() *Cache { return &Cache{ - chats: map[int64]*client.Chat{}, + ownChats: map[int64]*client.Chat{}, + auxChats: map[int64]*client.Chat{}, users: map[int64]*client.User{}, statuses: map[int64]*Status{}, } @@ -40,7 +42,23 @@ func (cache *Cache) ChatsKeys() []int64 { defer cache.chatsLock.Unlock() var keys []int64 - for id := range cache.chats { + for id := range cache.ownChats { + keys = append(keys, id) + } + for id := range cache.auxChats { + keys = append(keys, id) + } + return keys +} + +// OwnChatsKeys grabs only own chat ids synchronously to avoid lockups +// while they are used +func (cache *Cache) OwnChatsKeys() []int64 { + cache.chatsLock.Lock() + defer cache.chatsLock.Unlock() + + var keys []int64 + for id := range cache.ownChats { keys = append(keys, id) } return keys @@ -84,7 +102,10 @@ func (cache *Cache) GetChat(id int64) (*client.Chat, bool) { cache.chatsLock.Lock() defer cache.chatsLock.Unlock() - chat, ok := cache.chats[id] + chat, ok := cache.ownChats[id] + if !ok { + chat, ok = cache.auxChats[id] + } return chat, ok } @@ -107,11 +128,21 @@ func (cache *Cache) GetStatus(id int64) (*Status, bool) { } // SetChat stores a chat in the cache -func (cache *Cache) SetChat(id int64, chat *client.Chat) { +func (cache *Cache) SetChat(id int64, chat *client.Chat, own bool) { cache.chatsLock.Lock() defer cache.chatsLock.Unlock() - cache.chats[id] = chat + if own { + cache.ownChats[id] = chat + // move from aux to own, but not vice versa + // (own: true means that presences for the chat are needed + // for sure, false means just "not necessarily") + if _, ok := cache.auxChats[id]; ok { + delete(cache.auxChats, id) + } + } else { + cache.auxChats[id] = chat + } } // SetUser stores a user in the cache diff --git a/telegram/commands.go b/telegram/commands.go index ec1db89..1c066e0 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -218,7 +218,7 @@ func (c *Client) helpString(typ CommandType, chatId int64) string { var str strings.Builder commandMap := GetCommands(typ) - chatType, chatTypeErr := c.GetChatType(chatId) + chatType, chatTypeErr := c.GetChatType(chatId, true) str.WriteString("Available commands:\n") for _, name := range SortedCommandKeys(commandMap) { @@ -378,7 +378,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin return errors.Wrap(err, "Logout error").Error(), false } - for _, id := range c.cache.ChatsKeys() { + for _, id := range c.cache.OwnChatsKeys() { c.unsubscribe(id) } @@ -505,7 +505,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin return strings.Join(entries, "\n"), true case "report": - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), false } @@ -555,7 +555,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return notEnoughArguments, true, false } - chatType, chatTypeErr := c.GetChatType(chatID) + chatType, chatTypeErr := c.GetChatType(chatID, true) if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { return "Not applicable for this chat type", true, false } @@ -835,7 +835,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } // invite @username to current groupchat case "invite": - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), true, false } @@ -862,7 +862,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return link.InviteLink, true, true // kick @username from current group chat case "kick": - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), true, false } @@ -881,7 +881,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, // mute [@username [n hours]] case "mute": if len(args) > 0 { - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), true, false } @@ -918,7 +918,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, // unmute [@username] case "unmute": if len(args) > 0 { - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), true, false } @@ -946,7 +946,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } // ban @username from current chat [for N hours] case "ban": - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), true, false } @@ -974,7 +974,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } // unban @username case "unban": - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), true, false } @@ -992,7 +992,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } // promote @username to admin case "promote": - contact, _, err := c.GetContactByUsername(args[0]) + contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { return err.Error(), true, false } @@ -1064,7 +1064,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } // close secret chat case "close": - chat, _, err := c.GetContactByID(chatID, nil) + chat, _, err := c.GetContactByID(chatID, nil, true) if err != nil { return err.Error(), true, false } diff --git a/telegram/connect.go b/telegram/connect.go index d37c5fd..8a2b928 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -229,7 +229,7 @@ func (c *Client) Disconnect(resource string, quit bool) bool { log.Warn("Disconnecting from Telegram network...") // we're offline (unsubscribe if logout) - for _, id := range c.cache.ChatsKeys() { + for _, id := range c.cache.OwnChatsKeys() { args := gateway.SimplePresence(id, "unavailable") c.sendPresence(args...) } diff --git a/telegram/handlers.go b/telegram/handlers.go index 5a193f6..a9cdb06 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -186,13 +186,13 @@ func (c *Client) updateHandler() { func (c *Client) updateUser(update *client.UpdateUser) { c.cache.SetUser(update.User.Id, update.User) show, status, presenceType := c.userStatusToText(update.User.Status, update.User.Id) - go c.ProcessStatusUpdate(update.User.Id, status, show, gateway.SPType(presenceType)) + go c.ProcessStatusUpdate(update.User.Id, status, show, false, gateway.SPType(presenceType)) } // user status changed func (c *Client) updateUserStatus(update *client.UpdateUserStatus) { show, status, presenceType := c.userStatusToText(update.Status, update.UserId) - go c.ProcessStatusUpdate(update.UserId, status, show, gateway.SPImmed(false), gateway.SPType(presenceType)) + go c.ProcessStatusUpdate(update.UserId, status, show, false, gateway.SPImmed(false), gateway.SPType(presenceType)) } // new chat discovered @@ -206,14 +206,14 @@ func (c *Client) updateNewChat(update *client.UpdateNewChat) { } } - c.cache.SetChat(update.Chat.Id, update.Chat) + c.cache.SetChat(update.Chat.Id, update.Chat, true) if update.Chat.Positions != nil && len(update.Chat.Positions) > 0 { c.subscribeToID(update.Chat.Id, update.Chat) } if update.Chat.Id < 0 { - c.ProcessStatusUpdate(update.Chat.Id, update.Chat.Title, "chat") + c.ProcessStatusUpdate(update.Chat.Id, update.Chat.Title, "chat", true) } }() } @@ -377,7 +377,7 @@ func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { return } if c.Session.IgnoreGroupDeletions { - chatType, chatTypeErr := c.GetChatType(update.ChatId) + chatType, chatTypeErr := c.GetChatType(update.ChatId, false) if chatTypeErr == nil && (chatType == ChatTypeBasicGroup || chatType == ChatTypeSupergroup) { return } @@ -432,9 +432,9 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { gateway.SetNickname(c.jid, strconv.FormatInt(update.ChatId, 10), update.Title, c.xmpp) // set also the status (for group chats only) - chat, user, _ := c.GetContactByID(update.ChatId, nil) + chat, user, _ := c.GetContactByID(update.ChatId, nil, false) if user == nil { - c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", gateway.SPImmed(true)) + c.ProcessStatusUpdate(update.ChatId, update.Title, "chat", false, gateway.SPImmed(true)) } // update chat title in the cache diff --git a/telegram/utils.go b/telegram/utils.go index 3cf2a05..9177eb8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -94,7 +94,7 @@ const ( const AVATAR_SIZE_LIMIT int64 = 128 * 1024 // GetContactByUsername resolves username to user id retrieves user and chat information -func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { +func (c *Client) GetContactByUsername(username string, own bool) (*client.Chat, *client.User, error) { if !c.Online() { return nil, nil, errOffline } @@ -119,11 +119,11 @@ func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.Us } } - return c.GetContactByID(userID, chat) + return c.GetContactByID(userID, chat, own) } // GetContactByID gets user and chat information from cache (or tries to retrieve it, if missing) -func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *client.User, error) { +func (c *Client) GetContactByID(id int64, chat *client.Chat, own bool) (*client.Chat, *client.User, error) { if !c.Online() || id == 0 { return nil, nil, errOffline } @@ -158,9 +158,9 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli return nil, nil, err } - c.cache.SetChat(id, cacheChat) + c.cache.SetChat(id, cacheChat, own) } else { - c.cache.SetChat(id, chat) + c.cache.SetChat(id, chat, own) } } if chat == nil { @@ -171,7 +171,7 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli } // GetChatType obtains chat type from its information -func (c *Client) GetChatType(id int64) (ChatType, error) { +func (c *Client) GetChatType(id int64, own bool) (ChatType, error) { if !c.Online() || id == 0 { return ChatTypeUnknown, errOffline } @@ -187,7 +187,7 @@ func (c *Client) GetChatType(id int64) (ChatType, error) { return ChatTypeUnknown, err } - c.cache.SetChat(id, chat) + c.cache.SetChat(id, chat, own) } chatType := chat.Type.ChatTypeType() @@ -209,8 +209,8 @@ func (c *Client) GetChatType(id int64) (ChatType, error) { } // IsPM checks if a chat is PM -func (c *Client) IsPM(id int64) (bool, error) { - typ, err := c.GetChatType(id) +func (c *Client) IsPM(id int64, own bool) (bool, error) { + typ, err := c.GetChatType(id, own) if err != nil { return false, err } @@ -222,8 +222,8 @@ func (c *Client) IsPM(id int64) (bool, error) { } // IsBot checks if a chat is a bot -func (c *Client) IsBot(id int64) (bool, error) { - _, user, err := c.GetContactByID(id, nil) +func (c *Client) IsBot(id int64, own bool) (bool, error) { + _, user, err := c.GetContactByID(id, nil, own) if err != nil { return false, err } @@ -395,7 +395,7 @@ func (c *Client) GetPhotoBase64(photo *client.File) string { } // ProcessStatusUpdate sets contact status -func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, oldArgs ...args.V) error { +func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, own bool, oldArgs ...args.V) error { if !c.Online() { return nil } @@ -404,7 +404,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o "chat_id": chatID, }).Info("Status update for") - chat, user, err := c.GetContactByID(chatID, nil) + chat, user, err := c.GetContactByID(chatID, nil, own) if err != nil { return err } @@ -470,7 +470,7 @@ func (c *Client) FormatContact(chatID int64) string { return "" } - chat, user, err := c.GetContactByID(chatID, nil) + chat, user, err := c.GetContactByID(chatID, nil, false) if err != nil { return "unknown contact: " + err.Error() } @@ -1091,7 +1091,7 @@ func (c *Client) isCarbonsEnabled() bool { } func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, suppressReply bool) (string, *gateway.Reply) { - isPM, err := c.IsPM(message.ChatId) + isPM, err := c.IsPM(message.ChatId, true) if err != nil { log.Errorf("Could not determine if chat is PM: %v", err) } @@ -1215,8 +1215,8 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { ChatId: chatId, }) if err == nil { - c.cache.SetChat(chatId, chat) - go c.ProcessStatusUpdate(chatId, "", "", gateway.SPImmed(true)) + c.cache.SetChat(chatId, chat, true) + go c.ProcessStatusUpdate(chatId, "", "", true, gateway.SPImmed(true)) text = "" if chat.Photo == nil { @@ -1258,7 +1258,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { var ignorePrefix bool if oobSwap { if text == "" || message.Content.MessageContentType() == client.TypeMessageSticker { - chatType, err := c.GetChatType(chatId) + chatType, err := c.GetChatType(chatId, true) ignorePrefix = err == nil && (chatType != ChatTypeBasicGroup && chatType != ChatTypeSupergroup) && c.isCarbonsEnabled() } } @@ -1456,9 +1456,9 @@ 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() +// OwnChatsKeys proxies the following function from unexported cache +func (c *Client) OwnChatsKeys() []int64 { + return c.cache.OwnChatsKeys() } // StatusesRange proxies the following function from unexported cache @@ -1515,8 +1515,8 @@ func (c *Client) roster(resource string) { log.Warnf("Sending roster for %v", resource) - for _, chat := range c.cache.ChatsKeys() { - c.ProcessStatusUpdate(chat, "", "") + for _, chat := range c.cache.OwnChatsKeys() { + c.ProcessStatusUpdate(chat, "", "", true) } c.sendPresence(gateway.SPStatus("Logged in as: " + c.Session.Login)) @@ -1630,7 +1630,7 @@ func (c *Client) subscribeToID(id int64, chat *client.Chat) { args := gateway.SimplePresence(id, "subscribe") if chat == nil { - chat, _, _ = c.GetContactByID(id, nil) + chat, _, _ = c.GetContactByID(id, nil, true) } if chat != nil { args = append(args, gateway.SPNickname(chat.Title)) @@ -1660,7 +1660,7 @@ func (c *Client) prepareDiskSpace(size uint64) { func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { var info VCardInfo - chat, user, err := c.GetContactByID(toID, nil) + chat, user, err := c.GetContactByID(toID, nil, false) if err != nil { return info, err } @@ -1687,7 +1687,7 @@ func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { } func (c *Client) UpdateChatNicknames() { - for _, id := range c.cache.ChatsKeys() { + for _, id := range c.cache.OwnChatsKeys() { chat, ok := c.cache.GetChat(id) if ok { newArgs := []args.V{ @@ -1810,7 +1810,7 @@ func (c *Client) GetChatMembers(chatID int64, limited bool, query string, member if limited { limit = 20 - chat, _, err := c.GetContactByID(chatID, nil) + chat, _, err := c.GetContactByID(chatID, nil, true) if err != nil { return nil, err } else if chat == nil { diff --git a/xmpp/component.go b/xmpp/component.go index f0c481d..b7ccd7b 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -141,6 +141,7 @@ func heartbeat(component *xmpp.Component) { chatID, session.LastSeenStatus(delayedStatus.TimestampOnline), "away", + true, ) delete(session.DelayedStatuses, chatID) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 8e5da5c..5c00419 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -363,7 +363,7 @@ func handleSubscription(s xmpp.Sender, p stanza.Presence) { if !ok { return } - go session.ProcessStatusUpdate(toID, "", "", gateway.SPImmed(false)) + go session.ProcessStatusUpdate(toID, "", "", true, gateway.SPImmed(false)) } func handlePresence(s xmpp.Sender, p stanza.Presence) { @@ -434,6 +434,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { status.ID, description, show, + true, newArgs..., ) } @@ -548,7 +549,7 @@ func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGe if !ok { log.Info("Could not find avatar in cache, fetching immediately") - chat, _, err := session.GetContactByID(chatId, nil) + chat, _, err := session.GetContactByID(chatId, nil, true) if err != nil || chat == nil || chat.Photo == nil { return } @@ -609,7 +610,7 @@ func getTelegramChatType(from string, to string) (telegram.ChatType, error) { if ok { session, ok := sessions[bare] if ok { - return session.GetChatType(toId) + return session.GetChatType(toId, true) } } } @@ -713,7 +714,7 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { isOnline = session.Online() if toOk { - isBot, err := session.IsBot(toID) + isBot, err := session.IsBot(toID, true) if err == nil && isBot { di.AddItem(iq.To, "botmenu", "Bot Menu") } @@ -1377,8 +1378,8 @@ func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram. return } - for _, chatId := range session.ChatsKeys() { - chat, _, err := session.GetContactByID(chatId, nil) + for _, chatId := range session.OwnChatsKeys() { + chat, _, err := session.GetContactByID(chatId, nil, true) if err != nil || chat == nil { continue } From 6ceb4efe327ed1780fbb564a524482c61a62e900 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 18 Sep 2025 14:34:17 -0400 Subject: [PATCH 217/228] Fix before-after comparison in MAM --- xmpp/handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 7ad4d95..3039ff6 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2395,9 +2395,9 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) messages = telegram.ChronologicallySortMessages(messages, reverse) complete = true } else if beforeId != 0 || afterId != 0 { - if (beforeId != 0 && afterId != 0) && beforeId > afterId { + if (beforeId != 0 && afterId != 0) && beforeId < afterId { iqAnswerSetError(answer, 400) - log.Debugf("MAM: before > after") + log.Debugf("MAM: before < after") return } From 15f62b619d2399473837ffb1759aa30c698d57ab Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 18 Sep 2025 16:51:08 -0400 Subject: [PATCH 218/228] Fix complete flag logic: it marks if RSM/result covers full range, not if the output range touches the end --- telegram/utils.go | 29 ++++++++++++++++++++++++++++- xmpp/handlers.go | 7 ++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index a7d9efa..e30bdb8 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2399,6 +2399,7 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, var newMessages *client.Messages if limit == 0 { + complete = true return } @@ -2471,14 +2472,40 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, } else { messages = fetchedMessages } + if complete && len(messages) > 0 { + firstMessage := messages[len(messages)-1] + if fromMessageId > 1 { + if firstMessage.Id > fromMessageId { + complete = false + } + } else { + // try to fetch one more message to check if there are any other before + previousMessage, previousMessageErr := c.GetPreviousMessage(chatID, firstMessage.Id) + if previousMessageErr == nil && previousMessage != nil { + complete = false + } + } + } } else { - complete = true for _, message := range fetchedMessages { if message.Id <= fromMessageId { + complete = true break } messages = append(messages, message) } + if len(messages) > 0 { + firstMessage := messages[len(messages)-1] + // try to fetch one more message to check if there are any other before + previousMessage, previousMessageErr := c.GetPreviousMessage(chatID, firstMessage.Id) + if previousMessageErr == nil && previousMessage != nil { + if previousMessage.Id == fromMessageId { + complete = true + } + } else { + complete = true + } + } } if !reverse { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 3039ff6..e45edb1 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2372,6 +2372,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) var beyond, complete bool canBeComplete := true fromStart := !rsmLastPage + toEnd := rsmLastPage var overallyFirstMessageId, overallyLastMessageId int64 reverse := query.GetFlipPage() != nil @@ -2453,6 +2454,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) } if rsmAfter != afterId { fromStart = false + canBeComplete = false overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, afterId) if overallyFirstMessageErr == nil && overallyFirstMessage != nil { overallyFirstMessageId = overallyFirstMessage.Id @@ -2468,6 +2470,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) return } if rsmBefore < beforeId { + toEnd = false canBeComplete = false } beforeId = rsmBefore @@ -2537,6 +2540,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) if rsmAfterMessage.Id > fromMessageId && int64(rsmAfterMessage.Date) >= startTime.Unix() { fromStart = false + canBeComplete = false overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, fromMessageId) if overallyFirstMessageErr == nil && overallyFirstMessage != nil { overallyFirstMessageId = overallyFirstMessage.Id @@ -2564,6 +2568,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) if newLastMessageErr == nil && newLastMessage != nil { if lastMessageId == 0 || lastMessageId != newLastMessage.Id { canBeComplete = false + toEnd = false lastMessageId = newLastMessage.Id } } else { @@ -2637,7 +2642,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) overallyFirstMessageId = firstMsgId } } - if complete { + if toEnd { if reverse { overallyLastMessageId = firstMsgId } else { From 89d368abbb45ef879a8e0387227a0a17ec22fd50 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 19 Sep 2025 12:44:35 -0400 Subject: [PATCH 219/228] Yet another MAM logic rewrite: assume request order by RSM before/after, do not consider RSM limitation themselves as incompleteness --- telegram/utils.go | 3 ++- xmpp/handlers.go | 51 ++++++++++++++++++++++++----------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/telegram/utils.go b/telegram/utils.go index e30bdb8..82bb870 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2478,13 +2478,14 @@ func (c *Client) GetMessagesBetween(chatID, fromMessageId, lastMessageId int64, if firstMessage.Id > fromMessageId { complete = false } - } else { + } else if fromMessageId == 1 { // try to fetch one more message to check if there are any other before previousMessage, previousMessageErr := c.GetPreviousMessage(chatID, firstMessage.Id) if previousMessageErr == nil && previousMessage != nil { complete = false } } + // for 0 one last message is already fetched as there is at least one, right? } } else { for _, message := range fetchedMessages { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index e45edb1..a8b87e7 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -2344,18 +2344,10 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) } } - if rsmBefore != 0 && rsmAfter == 0 { - rsmLastPage = true - } - if rsmLimit == 0 && !justCount { rsmLimit = 100 } - if rsmLastPage { - rsmLimit = -rsmLimit // hacky, I know, and? :P - } - // check for mutual parameter compatibility, there's a lot of them, nah? if ((!startTime.IsZero() || !endTime.IsZero()) && (beforeId != 0 || afterId != 0 || ids != nil)) || ((beforeId != 0 || afterId != 0) && (!startTime.IsZero() || !endTime.IsZero() || ids != nil)) || @@ -2368,11 +2360,32 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) // dummy call to circumvent unexported type messages, _, err := session.GetMessagesBetween(toID, 0, 0, 0, false) + var order bool // false from start, true from end + // lower priority + if !endTime.IsZero() { + order = true + } + // higher priority + if beforeId != 0 { + order = false + } + if afterId != 0 { + order = true + } + if rsmAfter != 0 { + order = false + } + if rsmBefore != 0 || rsmLastPage { + order = true + } + if order { + rsmLimit = -rsmLimit // hacky, I know, and? :P + } + quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold)) var beyond, complete bool - canBeComplete := true - fromStart := !rsmLastPage - toEnd := rsmLastPage + fromStart := !order + toEnd := order var overallyFirstMessageId, overallyLastMessageId int64 reverse := query.GetFlipPage() != nil @@ -2454,7 +2467,6 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) } if rsmAfter != afterId { fromStart = false - canBeComplete = false overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, afterId) if overallyFirstMessageErr == nil && overallyFirstMessage != nil { overallyFirstMessageId = overallyFirstMessage.Id @@ -2471,7 +2483,6 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) } if rsmBefore < beforeId { toEnd = false - canBeComplete = false } beforeId = rsmBefore } @@ -2486,11 +2497,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) } if !beyond { - var newComplete bool - messages, newComplete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit, reverse) - if canBeComplete { - complete = newComplete - } + messages, complete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit, reverse) } } else { // time limit or no limits at all // don't allow to fetch far beyond the quota @@ -2540,7 +2547,6 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) if rsmAfterMessage.Id > fromMessageId && int64(rsmAfterMessage.Date) >= startTime.Unix() { fromStart = false - canBeComplete = false overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, fromMessageId) if overallyFirstMessageErr == nil && overallyFirstMessage != nil { overallyFirstMessageId = overallyFirstMessage.Id @@ -2567,7 +2573,6 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) newLastMessage, newLastMessageErr := session.GetPreviousMessage(toID, rsmBeforeMessage.Id) if newLastMessageErr == nil && newLastMessage != nil { if lastMessageId == 0 || lastMessageId != newLastMessage.Id { - canBeComplete = false toEnd = false lastMessageId = newLastMessage.Id } @@ -2584,11 +2589,7 @@ func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) } if !beyond { // yes🗿, twice - var newComplete bool - messages, newComplete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit, reverse) - if canBeComplete { - complete = newComplete - } + messages, complete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit, reverse) } } } From e7c6318e48fa4d89495b26adb9a210dbc7609a63 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 9 Oct 2025 11:32:33 -0400 Subject: [PATCH 220/228] Add /cancelauth command && unsubscribe from chats more eagerly --- Makefile | 2 +- telegabber.go | 2 +- telegram/commands.go | 14 +++++++++++--- telegram/connect.go | 4 +++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 3182744..71aeabf 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.6" +VERSION := "v1.12.7" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index cbc58b3..6fb5d5b 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.6" +var version string = "1.12.7" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 1c066e0..fa38efb 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -54,6 +54,7 @@ var transportCommands = map[string]command{ "help": command{0, []string{}, "help", false, nil}, "login": command{1, []string{"phone"}, "sign in", false, nil}, "logout": command{0, []string{}, "sign out", true, nil}, + "cleanup": command{0, []string{}, "unsubscribe from all known chats", false, 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}, @@ -279,6 +280,12 @@ func (c *Client) unsubscribe(chatID int64) error { return c.sendPresence(args...) } +func (c *Client) unsubscribeFromAll() { + for _, id := range c.cache.ChatsKeys() { + c.unsubscribe(id) + } +} + func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { for i := len(messages) - 1; i >= 0; i-- { message := messages[i] @@ -378,11 +385,12 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin return errors.Wrap(err, "Logout error").Error(), false } - for _, id := range c.cache.OwnChatsKeys() { - c.unsubscribe(id) - } + c.unsubscribeFromAll() c.Session.Login = "" + // cleanup + case "cleanup": + c.unsubscribeFromAll() // cancel auth case "cancelauth": if c.Online() { diff --git a/telegram/connect.go b/telegram/connect.go index 8a2b928..27d63c7 100644 --- a/telegram/connect.go +++ b/telegram/connect.go @@ -153,11 +153,13 @@ func (c *Client) Connect(resource string) error { c.addResource(resource) go func() { - _, err = c.client.GetChats(&client.GetChatsRequest{ + chats, err := c.client.GetChats(&client.GetChatsRequest{ Limit: chatsLimit, }) if err != nil { log.Errorf("Could not retrieve chats: %v", err) + } else { + log.Infof("Obtained ≈%v chats for initialization", chats.TotalCount) } gateway.SubscribeToTransport(c.xmpp, c.jid) From aaca93e66d10e55950b4f3c8f4542646b4d9cc34 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 10 Oct 2025 07:44:44 -0400 Subject: [PATCH 221/228] Respond to jabber:version (XEP-0092) queries --- telegabber.go | 2 +- xmpp/component.go | 3 ++- xmpp/gateway/gateway.go | 3 +++ xmpp/handlers.go | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/telegabber.go b/telegabber.go index 6fb5d5b..fefbc93 100644 --- a/telegabber.go +++ b/telegabber.go @@ -68,7 +68,7 @@ func main() { log.Infof("Starting telegabber version %v", version) - sm, component, err = xmpp.NewComponent(config.XMPP, config.Telegram, *idsPath) + sm, component, err = xmpp.NewComponent(config.XMPP, config.Telegram, *idsPath, version) if err != nil { log.Fatal(err) } diff --git a/xmpp/component.go b/xmpp/component.go index b7ccd7b..6a0ae98 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -39,10 +39,11 @@ var sizeRegex = regexp.MustCompile("\\A([0-9]+) ?([KMGTPE]?B?)\\z") // NewComponent starts a new component and wraps it in // a stream manager that you should start yourself -func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath string) (*xmpp.StreamManager, *xmpp.Component, error) { +func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath string, version string) (*xmpp.StreamManager, *xmpp.Component, error) { var err error gateway.Jid, err = stanza.NewJid(conf.Jid) + gateway.Version = version if err != nil { return nil, nil, err } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index 0b6b492..1efba08 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -50,6 +50,9 @@ var QueueLock = sync.Mutex{} // Jid stores the component's JID object var Jid *stanza.Jid +// Version stores this software's version +var Version string + // IdsDB provides a disk-backed bidirectional dictionary of Telegram and XMPP ids var IdsDB badger.IdsDB diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 5c00419..a926523 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -75,6 +75,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetQueryRegister(s, iq) return } + _, ok = iq.Payload.(*stanza.Version) + if ok { + go handleGetVersion(s, iq) + return + } } else if iq.Type == stanza.IQTypeSet { query, ok := iq.Payload.(*extensions.QueryRegister) if ok { @@ -643,6 +648,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { disco.AddFeatures("jabber:iq:register") } disco.AddFeatures(gateway.NSCommand) + disco.AddFeatures("jabber:iq:version") } else { chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) @@ -801,6 +807,32 @@ func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) { } } +func handleGetVersion(s xmpp.Sender, iq *stanza.IQ) { + 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 + } + + answer.Version().SetInfo(gateway.Jid.Resource, gateway.Version, "") + + log.Debugf("%#v", answer.Payload) + + _ = gateway.ResumableSend(component, answer) +} + func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { component, ok := s.(*xmpp.Component) if !ok { From 78a43058722d289adab18d9647e925aa2317f04a Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 18 Oct 2025 16:42:40 -0400 Subject: [PATCH 222/228] Respond to urn:xmpp:time (XEP-0202) queries --- telegram/commands.go | 6 +--- telegram/utils.go | 7 +++++ xmpp/extensions/extensions.go | 24 ++++++++++++++++ xmpp/handlers.go | 52 +++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index fa38efb..1f65520 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -667,11 +667,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, state = &client.MessageSchedulingStateSendWhenOnline{} result = due } else { - if c.Session.Timezone == "" { - due += "Z" - } else { - due += c.Session.Timezone - } + due += c.GetTZD() switch 0 { default: diff --git a/telegram/utils.go b/telegram/utils.go index 9177eb8..48e4b5c 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1792,6 +1792,13 @@ func (c *Client) usernamesToString(usernames []string) string { return strings.Join(atUsernames, ", ") } +func (c *Client) GetTZD() string { + if c.Session.Timezone == "" { + return "Z" + } + return c.Session.Timezone +} + // 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 diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 8e2f743..2509cf8 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -213,6 +213,14 @@ type QueryRegisterRemove struct { XMLName xml.Name `xml:"remove"` } +// EntityTime is from XEP-0202 +type EntityTime struct { + XMLName xml.Name `xml:"urn:xmpp:time time"` + Tzo string `xml:"tzo"` + Utc string `xml:"utc"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -278,6 +286,16 @@ func (c QueryRegister) GetSet() *stanza.ResultSet { return c.ResultSet } +// Namespace is a namespace! +func (c EntityTime) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c EntityTime) GetSet() *stanza.ResultSet { + return c.ResultSet +} + // Name is a packet name func (ClientMessage) Name() string { return "message" @@ -362,4 +380,10 @@ func init() { "jabber:iq:register", "query", }, QueryRegister{}) + + // entity time + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "urn:xmpp:time", + "time", + }, EntityTime{}) } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index a926523..b6d6822 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "sync" + "time" "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/telegram" @@ -80,6 +81,11 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetVersion(s, iq) return } + _, ok = iq.Payload.(*extensions.EntityTime) + if ok { + go handleGetEntityTime(s, iq) + return + } } else if iq.Type == stanza.IQTypeSet { query, ok := iq.Payload.(*extensions.QueryRegister) if ok { @@ -649,6 +655,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { } disco.AddFeatures(gateway.NSCommand) disco.AddFeatures("jabber:iq:version") + disco.AddFeatures("urn:xmpp:time") } else { chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) @@ -833,6 +840,51 @@ func handleGetVersion(s xmpp.Sender, iq *stanza.IQ) { _ = gateway.ResumableSend(component, answer) } +func handleGetEntityTime(s xmpp.Sender, iq *stanza.IQ) { + component, ok := s.(*xmpp.Component) + if !ok { + log.Error("Not a component") + return + } + + // separate declaration is crucial for passing as pointer to defer + var answer *stanza.IQ + var err error + 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) + + fromJid, err := stanza.NewJid(iq.From) + if err != nil { + log.Error("Invalid from JID!") + return + } + + session, ok := sessions[fromJid.Bare()] + if !ok { + log.Error("IQ from stranger") + return + } + + entityTime := extensions.EntityTime{ + Tzo: session.GetTZD(), + Utc: time.Now().UTC().Format(time.RFC3339), + } + answer.Payload = &entityTime + + log.Debugf("%#v", entityTime) +} + func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { component, ok := s.(*xmpp.Component) if !ok { From e073ded9e4cbcb4bbabff825f002233cb5cc4174 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 19 Oct 2025 13:07:16 -0400 Subject: [PATCH 223/228] Version 1.12.8 --- Makefile | 2 +- telegabber.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 71aeabf..4db1e45 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TD_COMMIT := "5bbfc1cf5dab94f82e02f3430ded7241d4653551" -VERSION := "v1.12.7" +VERSION := "v1.12.8" MAKEOPTS := "-j4" all: diff --git a/telegabber.go b/telegabber.go index fefbc93..8bd46f5 100644 --- a/telegabber.go +++ b/telegabber.go @@ -16,7 +16,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.12.7" +var version string = "1.12.8" var commit string var sm *goxmpp.StreamManager From 1d29aa4694117a014f1c7f20fff080ea50de8960 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 27 Oct 2025 17:52:46 -0400 Subject: [PATCH 224/228] Generic online safety check for commands --- telegram/commands.go | 173 +++++++++++++++++++++++-------------------- xmpp/handlers.go | 2 +- 2 files changed, 94 insertions(+), 81 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index 1f65520..f938d03 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -51,23 +51,23 @@ var permissionsMember = client.ChatPermissions{ var permissionsReadonly = client.ChatPermissions{} var transportCommands = map[string]command{ - "help": command{0, []string{}, "help", false, nil}, - "login": command{1, []string{"phone"}, "sign in", false, nil}, - "logout": command{0, []string{}, "sign out", true, nil}, - "cleanup": command{0, []string{}, "unsubscribe from all known chats", false, 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}, + "help": command{0, []string{}, "help", false, nil, false}, + "login": command{1, []string{"phone"}, "sign in", false, nil, false}, + "logout": command{0, []string{}, "sign out", true, nil, true}, + "cleanup": command{0, []string{}, "unsubscribe from all known chats", false, nil, false}, + "cancelauth": command{0, []string{}, "quit the signin wizard", false, nil, false}, + "code": command{1, []string{"xxxxx"}, "check one-time code", false, nil, false}, + "password": command{1, []string{"********"}, "check 2fa password", false, nil, false}, + "setusername": command{0, []string{"@username"}, "update @username", true, nil, true}, + "setname": command{1, []string{"first", "last"}, "update name", true, nil, false}, + "setbio": command{0, []string{"Lorem ipsum"}, "update about", true, nil, true}, + "setpassword": command{0, []string{"old", "new"}, "set or remove password", true, nil, true}, + "config": command{0, []string{"param", "value"}, "view or update configuration options", false, nil, false}, + "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username", true, nil, true}, + "add": command{1, []string{"@username"}, "add @username to your chat list", true, nil, true}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil, true}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", true, nil, true}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", true, nil, true}, } var notForGroups = []ChatType{ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} @@ -76,38 +76,38 @@ var notForPMAndBasic = []ChatType{ChatTypePrivate, ChatTypeSecret, ChatTypeBasic var onlyForSecret = []ChatType{ChatTypePrivate, ChatTypeBasicGroup, ChatTypeSupergroup, ChatTypeChannel} var chatCommands = map[string]command{ - "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}, - "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}, - "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}, + "help": command{0, []string{}, "help", false, nil, false}, + "d": command{0, []string{"n"}, "delete your last message(s)", true, nil, true}, + "s": command{1, []string{"edited message"}, "edit your last message", true, nil, true}, + "silent": command{1, []string{"message"}, "send a message without sound", true, nil, true}, + "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, true}, + "raw": command{1, []string{"message"}, "send a raw message not interpeted as a transport command (e.g. a bot command)", true, nil, true}, + "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", true, nil, true}, + "vcard": command{0, []string{}, "print vCard as text", true, nil, true}, + "add": command{1, []string{"@username"}, "add @username to your chat list", true, nil, true}, + "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil, true}, + "group": command{1, []string{"title"}, "create groupchat «title» with current user", true, ¬ForGroups, true}, + "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", true, nil, true}, + "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", true, nil, true}, + "secret": command{0, []string{}, "create secretchat with current user", true, ¬ForGroups, true}, + "search": command{0, []string{"string", "[limit]"}, "search in current chat", true, nil, true}, + "history": command{0, []string{"limit"}, "get last [limit] messages from current chat", true, nil, true}, + "block": command{0, []string{}, "blacklist current user", true, ¬ForGroups, true}, + "unblock": command{0, []string{}, "unblacklist current user", true, ¬ForGroups, true}, + "invite": command{1, []string{"id or @username"}, "add user to current chat", true, ¬ForPM, true}, + "link": command{0, []string{}, "get invite link for current chat", true, ¬ForPM, true}, + "kick": command{1, []string{"id or @username"}, "remove user from current chat", true, ¬ForPM, true}, + "mute": command{0, []string{"id or @username", "hours"}, "mute the whole chat or a user in current chat", true, ¬ForPMAndBasic, true}, + "unmute": command{0, []string{"id or @username"}, "unmute the whole chat or a user in the current chat", true, ¬ForPMAndBasic, true}, + "ban": command{1, []string{"id or @username", "hours"}, "restrict @username from current chat for [hours] or forever", true, ¬ForPM, true}, + "unban": command{1, []string{"id or @username"}, "unbans @username in current chat (and devotes from admins)", true, ¬ForPM, true}, + "promote": command{1, []string{"id or @username", "title"}, "promote user to admin in current chat", true, ¬ForPM, true}, + "leave": command{0, []string{}, "leave current chat", true, ¬ForPM, true}, + "leave!": command{0, []string{}, "leave current chat (for owners)", true, ¬ForPM, true}, + "ttl": command{0, []string{"seconds"}, "set secret chat messages TTL before self-destroying", true, &onlyForSecret, true}, + "close": command{0, []string{}, "close current secret chat", true, &onlyForSecret, true}, + "delete": command{0, []string{}, "delete current chat from chat list", true, nil, true}, + "members": command{0, []string{"query"}, "search members [by optional query] in current chat (requires admin rights)", true, nil, true}, } var transportConfigurationOptions = map[string]configurationOption{ @@ -129,6 +129,7 @@ type command struct { Description string LoginOnly bool NotFor *[]ChatType + OnlineOnly bool } type configurationOption struct { arguments string @@ -143,6 +144,15 @@ const ( CommandTypeChat ) +// OnlineFilter is a tri-state condition for commands selection +type OnlineFilter int + +const ( + OnlineFilterOnline OnlineFilter = iota + OnlineFilterNotOnline + OnlineFilterAny +) + // GetCommands exposes the set of commands func GetCommands(typ CommandType) map[string]command { var commandMap map[string]command @@ -165,14 +175,20 @@ func GetCommand(typ CommandType, cmd string) (command, bool) { } // SortedCommandKeys sorts a slice with command keys -func SortedCommandKeys(commandMap map[string]command) []string { +func SortedCommandKeys(commandMap map[string]command, onlineFilter OnlineFilter) []string { keys := make([]string, len(commandMap)) i := 0 for k := range commandMap { + command := commandMap[k] + if (onlineFilter == OnlineFilterOnline && !command.OnlineOnly) || (onlineFilter == OnlineFilterNotOnline && command.OnlineOnly) { + continue + } + keys[i] = k i++ } + keys = keys[:i] sort.Strings(keys) @@ -215,24 +231,32 @@ func IsCommandForChatType(cmd command, chatType ChatType) bool { return true } -func (c *Client) helpString(typ CommandType, chatId int64) string { - var str strings.Builder - - commandMap := GetCommands(typ) - chatType, chatTypeErr := c.GetChatType(chatId, true) - - str.WriteString("Available commands:\n") - for _, name := range SortedCommandKeys(commandMap) { +func commandsToHelpString(str *strings.Builder, chatType ChatType, onlineFilter OnlineFilter, commandMap map[string]command) { + for _, name := range SortedCommandKeys(commandMap, onlineFilter) { command := commandMap[name] - if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { + if !IsCommandForChatType(command, chatType) { continue } str.WriteString(CommandToHelpString(name, command)) str.WriteString("\n") } +} + +func (c *Client) helpString(typ CommandType, chatId int64) string { + var str strings.Builder + + commandMap := GetCommands(typ) + chatType, _ := c.GetChatType(chatId, true) + + str.WriteString("Available commands:\n") if typ == CommandTypeTransport { - str.WriteString("Configuration options\n") + commandsToHelpString(&str, chatType, OnlineFilterNotOnline, commandMap) + + str.WriteString("\nOnline-only commands:\n") + commandsToHelpString(&str, chatType, OnlineFilterOnline, commandMap) + + str.WriteString("\nConfiguration options\n") for _, name := range persistence.ConfigKeys { option := transportConfigurationOptions[name] str.WriteString(name) @@ -242,6 +266,8 @@ func (c *Client) helpString(typ CommandType, chatId int64) string { str.WriteString(option.description) str.WriteString("\n") } + } else if typ == CommandTypeChat { + commandsToHelpString(&str, chatType, OnlineFilterAny, commandMap) } str.WriteString("\nYou may use ! instead of / if it conflicts with internal commands of a client") @@ -336,6 +362,9 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin if len(args) < command.RequiredArgs { return notEnoughArguments, false } + if command.OnlineOnly && !c.Online() { + return notOnline, false + } switch cmd { case "login", "code", "password": @@ -376,10 +405,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } // sign out case "logout": - if !c.Online() { - return notOnline, false - } - _, err := c.client.LogOut() if err != nil { return errors.Wrap(err, "Logout error").Error(), false @@ -400,10 +425,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin return "Cancelled", true // set @username case "setusername": - if !c.Online() { - return notOnline, false - } - var username string if len(args) > 0 { username = args[0] @@ -448,10 +469,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } // set About case "setbio": - if !c.Online() { - return notOnline, false - } - _, err := c.client.SetBio(&client.SetBioRequest{ Bio: rawCmdArguments(cmdline, 0), }) @@ -460,10 +477,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } // set password case "setpassword": - if !c.Online() { - return notOnline, false - } - var oldPassword string var newPassword string if len(args) > 0 { @@ -550,10 +563,6 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin // ProcessChatCommand executes a command sent in a mapped chat // 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, false - } - cmd, args := parseCommand(cmdline) command, ok := chatCommands[cmd] if !ok { @@ -562,6 +571,10 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, if len(args) < command.RequiredArgs { return notEnoughArguments, true, false } + if command.OnlineOnly && !c.Online() { + return notOnline, true, false + } + chatType, chatTypeErr := c.GetChatType(chatID, true) if chatTypeErr == nil && !IsCommandForChatType(command, chatType) { diff --git a/xmpp/handlers.go b/xmpp/handlers.go index b6d6822..4d5dcda 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -740,7 +740,7 @@ func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { } commands := telegram.GetCommands(cmdType) - for _, name := range telegram.SortedCommandKeys(commands) { + for _, name := range telegram.SortedCommandKeys(commands, telegram.OnlineFilterAny) { command := commands[name] if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) { continue From 5650850be9d5554617c381ef98994f42149f282b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 27 Oct 2025 18:21:49 -0400 Subject: [PATCH 225/228] Return text acknowledges for arbitrary commands --- telegram/commands.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/telegram/commands.go b/telegram/commands.go index f938d03..738c46b 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -403,6 +403,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin c.authorizer.Password <- args[0] } } + return "", true // sign out case "logout": _, err := c.client.LogOut() @@ -620,6 +621,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, if err != nil { return err.Error(), true, false } + return "", true, true // edit message case "s": if c.me == nil { @@ -653,6 +655,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } else { return "Message processing error", true, false } + return "", true, true // send without sound case "silent": content := c.PrepareOutgoingMessageContent(rawCmdArguments(cmdline, 0)) @@ -758,6 +761,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } else { return "Message processing error", true, false } + return "", true, true // forward a message to chat case "forward": messageId, err := strconv.ParseInt(args[0], 10, 64) @@ -1140,6 +1144,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } c.sendMessagesReverse(chatID, messages.Messages) + return "", true, true // get latest entries from history case "history": var limit int32 = 10 @@ -1176,6 +1181,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } c.sendMessagesReverse(chatID, messages) + return "", true, true // chat members case "members": var query string @@ -1205,7 +1211,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, return "", false, false } - return "", true, true + return "Success", true, true } func (c *Client) cmdAdd(args []string) (string, bool) { @@ -1221,7 +1227,7 @@ func (c *Client) cmdAdd(args []string) (string, bool) { c.subscribeToID(chat.Id, chat) - return "", true + return "Subscription sent", true } func (c *Client) cmdJoin(args []string) (string, bool) { @@ -1250,7 +1256,7 @@ func (c *Client) cmdJoin(args []string) (string, bool) { } } - return "", true + return "Joined", true } func (c *Client) cmdSupergroup(args []string, cmdline string) (string, bool) { @@ -1262,7 +1268,7 @@ func (c *Client) cmdSupergroup(args []string, cmdline string) (string, bool) { return err.Error(), false } - return "", true + return "Created", true } func (c *Client) cmdChannel(args []string, cmdline string) (string, bool) { @@ -1275,5 +1281,5 @@ func (c *Client) cmdChannel(args []string, cmdline string) (string, bool) { return err.Error(), false } - return "", true + return "Created", true } From eee277e36eb9b6383e937fb86fcfb9684df447be Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Fri, 31 Oct 2025 13:16:56 -0400 Subject: [PATCH 226/228] Track updated message ids for more reliable message edit check --- telegram/client.go | 39 +++++++++++ telegram/handlers.go | 154 ++++++++++++++++++++++++++++++------------- xmpp/component.go | 12 ++++ 3 files changed, 158 insertions(+), 47 deletions(-) diff --git a/telegram/client.go b/telegram/client.go index 005ec2f..4ef78c9 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -27,6 +27,41 @@ type HashedAvatar struct { File int32 } +// NewId stores message ids and timestamps of their additions so old ones can be truncated to save memory +type newId struct { + Id int64 + Ts int64 + lock sync.Mutex + ownLock sync.Mutex + locked bool + fired bool +} + +func newNewId() *newId { + return &newId{Ts: time.Now().Unix()} +} + +func (i *newId) Lock() { + i.ownLock.Lock() + if i.fired { + i.ownLock.Unlock() + return + } + i.locked = true + i.ownLock.Unlock() + i.lock.Lock() +} + +func (i *newId) Unlock() { + i.ownLock.Lock() + if i.locked { + i.lock.Unlock() + i.locked = false + i.fired = true + } + i.ownLock.Unlock() +} + // Client stores the metadata for lazily invoked TDlib instance type Client struct { client *client.Client @@ -64,6 +99,9 @@ type Client struct { AvatarHashes map[int64]*HashedAvatar AvatarHashesLock sync.Mutex + MessageIdChanges map[int64]map[int64]*newId + MessageIdChangesLock sync.Mutex + locks clientLocks SendMessageLock sync.Mutex } @@ -149,6 +187,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component lastMsgIds: make(map[int64]string), XmppClientFeatures: make(map[string]*[]string), AvatarHashes: make(map[int64]*HashedAvatar), + MessageIdChanges: make(map[int64]map[int64]*newId), locks: clientLocks{ chatMessageLocks: make(map[int64]*sync.Mutex), }, diff --git a/telegram/handlers.go b/telegram/handlers.go index a9cdb06..d7d0823 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -312,67 +312,99 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { sId := strconv.FormatInt(update.MessageId, 10) var isCarbon bool - message, messageErr := c.client.GetMessage(&client.GetMessageRequest{ - ChatId: update.ChatId, - MessageId: update.MessageId, - }) - var prefix string - if messageErr == nil { - if message.EditDate == 0 { - return + go func() { + message, messageErr := c.client.GetMessage(&client.GetMessageRequest{ + ChatId: update.ChatId, + MessageId: update.MessageId, + }) + if messageErr != nil { + // odnako za vremya puti + // sobaka mogla podrasti + c.MessageIdChangesLock.Lock() + idsMap, idsMapOk := c.MessageIdChanges[update.ChatId] + hadNoId := false + if idsMapOk { + newId, newIdOk := idsMap[update.MessageId] + if newIdOk { + if newId.Id == 0 { + hadNoId = true + c.MessageIdChangesLock.Unlock() + newId.Lock() + } + log.Infof("falling back to updated message id: %v/%v->%v", update.ChatId, update.MessageId, newId.Id) + message, messageErr = c.client.GetMessage(&client.GetMessageRequest{ + ChatId: update.ChatId, + MessageId: newId.Id, + }) + } + } + if !hadNoId { + c.MessageIdChangesLock.Unlock() + } } + var prefix string + if messageErr == nil { + if message.EditDate == 0 { + return + } + log.Debugf("editDate: %v", message.EditDate) - 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 is a carbon and if it is edited", update.ChatId, update.MessageId) - } - - // 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 + 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.Infof("Mismatching message ids: %v %v, falling back to separate edit message", lastXmppId, xmppId) + log.Errorf("No message %v/%v found, cannot reliably determine if it is a carbon and if it is edited: %v", update.ChatId, update.MessageId, messageErr.Error()) } - } - var text strings.Builder - - if replaceId == "" { - var editChar string - if c.Session.AsciiArrows { - editChar = "e" - } else { - editChar = "✎" + // 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 + } else { + log.Infof("Mismatching message ids: %v %v, falling back to separate edit message", lastXmppId, xmppId) + } } - 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, - )) + var text strings.Builder - sChatId := strconv.FormatInt(update.ChatId, 10) - for _, jid := range jids { - gateway.SendMessage(jid, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false) - } + if replaceId == "" { + var editChar string + if c.Session.AsciiArrows { + editChar = "e" + } else { + editChar = "✎" + } + 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, sChatId, text.String(), "e"+sId, c.xmpp, nil, replaceId, isCarbon, false) + } + }() } } // message(s) deleted func (c *Client) updateDeleteMessages(update *client.UpdateDeleteMessages) { if update.IsPermanent { + for _, deleteId := range update.MessageIds { + c.tryUnlockMessageId(update.ChatId, deleteId) + } + if c.Session.IsChatIgnored(update.ChatId) { return } @@ -411,6 +443,20 @@ func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucc log.Errorf("failed to replace %v with %v: %v", update.OldMessageId, update.Message.Id, err.Error()) } + c.MessageIdChangesLock.Lock() + idsMap, ok := c.MessageIdChanges[update.Message.ChatId] + if !ok { + idsMap = make(map[int64]*newId) + c.MessageIdChanges[update.Message.ChatId] = idsMap + } + id, ok := idsMap[update.OldMessageId] + if !ok { + id = newNewId() + idsMap[update.OldMessageId] = id + } + id.Id = update.Message.Id + c.MessageIdChangesLock.Unlock() + c.sendMarker(update.Message.ChatId, update.Message.Id, gateway.MarkerTypeReceived) // clean uploaded files @@ -420,6 +466,8 @@ func (c *Client) updateMessageSendSucceeded(update *client.UpdateMessageSendSucc } } func (c *Client) updateMessageSendFailed(update *client.UpdateMessageSendFailed) { + c.tryUnlockMessageId(update.Message.ChatId, update.OldMessageId) + // clean uploaded files file, _ := c.contentToFile(update.Message.Content) if file != nil && file.Local != nil { @@ -446,3 +494,15 @@ func (c *Client) updateChatTitle(update *client.UpdateChatTitle) { func (c *Client) updateChatReadOutbox(update *client.UpdateChatReadOutbox) { c.sendMarker(update.ChatId, update.LastReadOutboxMessageId, gateway.MarkerTypeDisplayed) } + +func (c *Client) tryUnlockMessageId(chatId, messageId int64) { + c.MessageIdChangesLock.Lock() + idsMap, ok := c.MessageIdChanges[chatId] + if ok { + id, ok := idsMap[messageId] + if ok { + id.Unlock() + } + } + c.MessageIdChangesLock.Unlock() +} diff --git a/xmpp/component.go b/xmpp/component.go index 6a0ae98..784b25f 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -148,6 +148,18 @@ func heartbeat(component *xmpp.Component) { } } session.DelayedStatusesLock.Unlock() + + // shrink message id maps + session.MessageIdChangesLock.Lock() + for _, idsMap := range session.MessageIdChanges { + for oldMessageId, newId := range idsMap { + if newId.Ts < now - 60 { + newId.Unlock() + delete(idsMap, oldMessageId) + } + } + } + session.MessageIdChangesLock.Unlock() } sessionLock.Unlock() From cf2183e9ca3308b807939b065fe95602ec9f72c3 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 3 Nov 2025 15:48:54 -0500 Subject: [PATCH 227/228] Add /status transport command --- telegram/commands.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telegram/commands.go b/telegram/commands.go index 5c905dc..d97bea9 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -63,6 +63,7 @@ var transportCommands = map[string]command{ "setbio": command{0, []string{"Lorem ipsum"}, "update about", true, nil, true}, "setpassword": command{0, []string{"old", "new"}, "set or remove password", true, nil, true}, "config": command{0, []string{"param", "value"}, "view or update configuration options", false, nil, false}, + "status": command{0, []string{}, "display current login stage", false, nil, false}, "report": command{2, []string{"chat", "comment"}, "report a chat by id or @username", true, nil, true}, "add": command{1, []string{"@username"}, "add @username to your chat list", true, nil, true}, "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil, true}, @@ -517,6 +518,8 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin } return strings.Join(entries, "\n"), true + case "status": + return fmt.Sprintf("Login stage: %v\nLogin: %v", c.loginStage, c.Session.Login), true case "report": contact, _, err := c.GetContactByUsername(args[0], false) if err != nil { From ade944a9d666253969637717b9cd8a41d8e5692b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 6 Dec 2025 08:11:59 -0500 Subject: [PATCH 228/228] Ping XEP-0363 link back for files uploaded in MUCs --- telegram/client.go | 4 ++++ telegram/handlers.go | 18 ++++++++++++++++++ telegram/utils.go | 24 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/telegram/client.go b/telegram/client.go index e9e1954..fa98135 100644 --- a/telegram/client.go +++ b/telegram/client.go @@ -148,6 +148,8 @@ type Client struct { mucCache map[int64]*MUCState + uploadingFiles map[int32]string + LastBotCmdString string XmppClientFeatures map[string]*[]string @@ -174,6 +176,7 @@ type clientLocks struct { lastMsgHashesLock sync.Mutex lastMsgIdsLock sync.RWMutex loginFinish barrier + uploadingFilesLock sync.Mutex authorizerReadLock sync.Mutex authorizerWriteLock sync.Mutex @@ -243,6 +246,7 @@ func NewClient(conf config.TelegramConfig, jid string, component *xmpp.Component editOutbox: make(map[string]string), pinOutbox: make(map[IntPair]chan int64), mucCache: make(map[int64]*MUCState), + uploadingFiles: make(map[int32]string), options: options, DelayedStatuses: make(map[int64]*DelayedStatus), lastMsgHashes: make(map[int64]uint64), diff --git a/telegram/handlers.go b/telegram/handlers.go index f63ef35..70fa8cf 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -136,6 +136,9 @@ func (c *Client) updateHandler() { case client.TypeUpdateChatPermissions: typedUpdate, _ := update.(*client.UpdateChatPermissions) c.updateChatPermissions(typedUpdate) + case client.TypeUpdateFile: + typedUpdate, _ := update.(*client.UpdateFile) + c.updateFile(typedUpdate) default: // log only handled types continue @@ -613,6 +616,21 @@ func (c *Client) updateChatPermissions(update *client.UpdateChatPermissions) { } } +func (c *Client) updateFile(update *client.UpdateFile) { + if update.File != nil && update.File.Local != nil { + // not really needed, why did I even write this then lol (TODO: maybe clean by some heur anyway) + /* c.locks.uploadingFilesLock.Lock() + if _, ok := c.uploadingFiles[update.File.Id]; ok && update.File.Local.CanBeDeleted && update.File.Local.Path != "" { + err := os.Remove(update.File.Local.Path) + if err != nil { + log.Warningf("Couldn't delete uploaded file: %v", err.Error()) + } + delete(c.uploadingFiles, update.File.Id) + } + c.locks.uploadingFilesLock.Unlock() */ + } +} + func (c *Client) tryUnlockMessageId(chatId, messageId int64) { c.MessageIdChangesLock.Lock() idsMap, ok := c.MessageIdChanges[chatId] diff --git a/telegram/utils.go b/telegram/utils.go index 04821fd..608b84a 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -1280,6 +1280,7 @@ func (c *Client) formatFile(file *client.File, compact bool) (string, string) { if file == nil { return "", "" } + log.Debugf("formatFile: %v %#v %#v", c.jid, file.Local, file.Remote) src, link := c.PermastoreFile(file, false) if compact { @@ -1313,6 +1314,16 @@ func (c *Client) PermastoreFile(file *client.File, clone bool) (string, string) size64 := uint64(file.Size) c.prepareDiskSpace(size64) + // detect uploading files, there's no remote id for them yet + c.locks.uploadingFilesLock.Lock() + var ok bool + link, ok = c.uploadingFiles[file.Id] + if ok && !file.Local.CanBeDeleted { + defer c.locks.uploadingFilesLock.Unlock() + return src, link + } + c.locks.uploadingFilesLock.Unlock() + basename := file.Remote.UniqueId + filepath.Ext(src) dest := c.content.Path + "/" + basename // destination path link = c.content.Link + "/" + basename // download link @@ -1792,6 +1803,8 @@ func (c *Client) ensureDownloadFile(file *client.File) *client.File { newFile, err := c.DownloadFile(file.Id, 1, true) if err == nil { return newFile + } else { + log.Errorf("Couldn't force-download file: %v", err.Error()) } } @@ -2113,6 +2126,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str // attach a file var file *client.InputFileLocal + link := text if c.content.Upload != "" && strings.HasPrefix(text, c.content.Upload) { response, err := http.Get(text) if err != nil { @@ -2183,6 +2197,16 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str c.returnError(returnJid, chatID, "Not sent", err, 400, isGroupchat) return nil, false } + + if file != nil { + document, ok := tgMessage.Content.(*client.MessageDocument) + if ok && document.Document != nil && document.Document.Document != nil { + c.locks.uploadingFilesLock.Lock() + c.uploadingFiles[document.Document.Document.Id] = link + c.locks.uploadingFilesLock.Unlock() + } + } + return tgMessage, false }