From 4a5b83dff5c568871d5624202e2ee27e5d0de242 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sun, 5 Mar 2023 03:00:53 -0500 Subject: Show XEP-0461 replies from Telegram --- xmpp/extensions/extensions.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'xmpp/extensions') diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index fac5e7b..ce27656 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -111,6 +111,13 @@ type IqVcardDesc struct { Text string `xml:",chardata"` } +// Reply is from XEP-0461 +type Reply struct { + XMLName xml.Name `xml:"urn:xmpp:reply:0 reply"` + To string `xml:"to,attr"` + Id string `xml:"id,attr"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -131,6 +138,11 @@ func (c IqVcardTemp) GetSet() *stanza.ResultSet { return c.ResultSet } +// Namespace is a namespace! +func (c Reply) Namespace() string { + return c.XMLName.Space +} + func init() { // presence nick stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{ @@ -149,4 +161,10 @@ func init() { "vcard-temp", "vCard", }, IqVcardTemp{}) + + // reply + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:reply:0", + "reply", + }, Reply{}) } -- cgit v1.2.3 From b1135b070b751998cb98eded6d29bdd7b800ecb9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 8 Mar 2023 07:51:19 -0500 Subject: Cut fallback quotes out --- telegram/utils.go | 37 +++++++++++++++++++++++++--------- xmpp/extensions/extensions.go | 47 +++++++++++++++++++++++++++++++++++++++++++ xmpp/gateway/gateway.go | 5 +++++ 3 files changed, 80 insertions(+), 9 deletions(-) (limited to 'xmpp/extensions') diff --git a/telegram/utils.go b/telegram/utils.go index f06abd7..fe6ca14 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -30,6 +30,7 @@ var spaceRegex = regexp.MustCompile(`\s+`) var replyRegex = regexp.MustCompile("\\A>>? ?([0-9]+)\\n") const newlineChar string = "\n" +const messageHeaderSeparator string = " | " // GetContactByUsername resolves username to user id retrieves user and chat information func (c *Client) GetContactByUsername(username string) (*client.Chat, *client.User, error) { @@ -710,7 +711,15 @@ func (c *Client) contentToFile(content client.MessageContent) (*client.File, *cl return nil, nil } -func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) string { +func (c *Client) countCharsInLines(lines *[]string) (count int) { + for _, line := range *lines { + count += len(line) + } + return +} + +func (c *Client) messageToPrefix(message *client.Message, previewString string, fileString string, replyMsg *client.Message) (string, int, int) { + var replyStart, replyEnd int prefix := []string{} // message direction var directionChar string @@ -734,7 +743,10 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } // reply to if message.ReplyToMessageId != 0 { - prefix = append(prefix, "reply: "+c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg)) + replyStart = c.countCharsInLines(&prefix) + (len(prefix) - 1) * len(messageHeaderSeparator) + replyLine := "reply: "+c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg) + prefix = append(prefix, replyLine) + replyEnd = replyStart + len(replyLine) + len(messageHeaderSeparator) } if message.ForwardInfo != nil { prefix = append(prefix, "fwd: "+c.formatForward(message.ForwardInfo)) @@ -748,7 +760,7 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, prefix = append(prefix, "file: "+fileString) } - return strings.Join(prefix, " | ") + return strings.Join(prefix, messageHeaderSeparator), replyStart, replyEnd } func (c *Client) ensureDownloadFile(file *client.File) *client.File { @@ -805,19 +817,26 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { } text = oob } else if !c.Session.RawMessages { - var prefix strings.Builder - prefix.WriteString(c.messageToPrefix(message, previewName, fileName, replyMsg)) + var newText strings.Builder + + prefix, replyStart, replyEnd := c.messageToPrefix(message, previewName, fileName, replyMsg) + 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 if chatId < 0 { - prefix.WriteString("\n") + newText.WriteString("\n") } else if chatId > 0 { - prefix.WriteString(" | ") + newText.WriteString(" | ") } - prefix.WriteString(text) + newText.WriteString(text) } - text = prefix.String() + text = newText.String() } } } diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index ce27656..7e44bae 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -2,6 +2,7 @@ package extensions import ( "encoding/xml" + "strconv" "gosrc.io/xmpp/stanza" ) @@ -118,6 +119,28 @@ type Reply struct { Id string `xml:"id,attr"` } +// Fallback is from XEP-0428 +type Fallback struct { + XMLName xml.Name `xml:"urn:xmpp:fallback:0 fallback"` + For string `xml:"for,attr"` + Body []FallbackBody + Subject []FallbackSubject +} + +// FallbackBody is from XEP-0428 +type FallbackBody struct { + XMLName xml.Name `xml:"body"` + Start string `xml:"start,attr"` + End string `xml:"end,attr"` +} + +// FallbackSubject is from XEP-0428 +type FallbackSubject struct { + XMLName xml.Name `xml:"subject"` + Start string `xml:"start,attr"` + End string `xml:"end,attr"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -143,6 +166,24 @@ func (c Reply) Namespace() string { return c.XMLName.Space } +// Namespace is a namespace! +func (c Fallback) Namespace() string { + return c.XMLName.Space +} + +// NewReplyFallback initializes a fallback range +func NewReplyFallback(start uint64, end uint64) Fallback { + return Fallback{ + For: "urn:xmpp:reply:0", + Body: []FallbackBody{ + FallbackBody{ + Start: strconv.FormatUint(start, 10), + End: strconv.FormatUint(end, 10), + }, + }, + } +} + func init() { // presence nick stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{ @@ -167,4 +208,10 @@ func init() { "urn:xmpp:reply:0", "reply", }, Reply{}) + + // fallback + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:fallback:0", + "fallback", + }, Fallback{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index d309ade..de8b495 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -16,6 +16,8 @@ import ( type Reply struct { Author string Id string + Start uint64 + End uint64 } const NSNick string = "http://jabber.org/protocol/nick" @@ -89,6 +91,9 @@ func sendMessageWrapper(to string, from string, body string, id string, componen To: reply.Author, Id: reply.Id, }) + if reply.End > 0 { + message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) + } } sendMessage(&message, component) -- cgit v1.2.3 From 90807b2d9e0565629a913d3b28b09c5fc9d746e6 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 14 Mar 2023 17:16:02 -0400 Subject: Convert XEP-0461 replies to Telegram --- telegabber.go | 2 +- telegram/commands.go | 6 +++--- telegram/utils.go | 14 +++++++++----- xmpp/extensions/extensions.go | 12 ++++++------ xmpp/handlers.go | 41 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 59 insertions(+), 16 deletions(-) (limited to 'xmpp/extensions') diff --git a/telegabber.go b/telegabber.go index 295ec40..0554cb9 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.4.0" +var version string = "1.5.0-dev" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 943d071..160e486 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -488,7 +488,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Last message is empty", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "") + content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", 0) if content != nil { c.client.EditMessageText(&client.EditMessageTextRequest{ @@ -505,7 +505,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) return "Not enough arguments", true } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "") + content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 0), "", 0) if content != nil { _, err := c.client.SendMessage(&client.SendMessageRequest{ @@ -584,7 +584,7 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) } } - content := c.ProcessOutgoingMessage(0, rawCmdArguments(cmdline, 1), "") + 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 fe6ca14..68dd524 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -857,7 +857,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) client.InputMessageContent { +func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid string, replyId int64) client.InputMessageContent { if !c.Online() { // we're offline return nil @@ -879,9 +879,13 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str // quotations var reply int64 - replySlice := replyRegex.FindStringSubmatch(text) - if len(replySlice) > 1 { - reply, _ = strconv.ParseInt(replySlice[1], 10, 64) + if replyId == 0 { + replySlice := replyRegex.FindStringSubmatch(text) + if len(replySlice) > 1 { + reply, _ = strconv.ParseInt(replySlice[1], 10, 64) + } + } else { + reply = replyId } // attach a file @@ -949,7 +953,7 @@ func (c *Client) ProcessOutgoingMessage(chatID int64, text string, returnJid str } // remove first line from text - if file != nil || reply != 0 { + if file != nil || (reply != 0 && replyId == 0) { newlinePos := strings.Index(text, newlineChar) if newlinePos != -1 { text = text[newlinePos+1:] diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 7e44bae..c982581 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -121,22 +121,22 @@ type Reply struct { // Fallback is from XEP-0428 type Fallback struct { - XMLName xml.Name `xml:"urn:xmpp:fallback:0 fallback"` - For string `xml:"for,attr"` - Body []FallbackBody - Subject []FallbackSubject + XMLName xml.Name `xml:"urn:xmpp:fallback:0 fallback"` + For string `xml:"for,attr"` + Body []FallbackBody `xml:"urn:xmpp:fallback:0 body"` + Subject []FallbackSubject `xml:"urn:xmpp:fallback:0 subject"` } // FallbackBody is from XEP-0428 type FallbackBody struct { - XMLName xml.Name `xml:"body"` + XMLName xml.Name `xml:"urn:xmpp:fallback:0 body"` Start string `xml:"start,attr"` End string `xml:"end,attr"` } // FallbackSubject is from XEP-0428 type FallbackSubject struct { - XMLName xml.Name `xml:"subject"` + XMLName xml.Name `xml:"urn:xmpp:fallback:0 subject"` Start string `xml:"start,attr"` End string `xml:"end,attr"` } diff --git a/xmpp/handlers.go b/xmpp/handlers.go index b023bc5..acf62a2 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -99,7 +99,46 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { toID, ok := toToID(msg.To) if ok { - session.ProcessOutgoingMessage(toID, msg.Body, msg.From) + var reply extensions.Reply + var fallback extensions.Fallback + msg.Get(&reply) + msg.Get(&fallback) + log.Debugf("reply: %#v", reply) + log.Debugf("fallback: %#v", fallback) + + var replyId int64 + 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!")) + } + + if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 { + body := fallback.Body[0] + var start, end int64 + start, err = strconv.ParseInt(body.Start, 10, 64) + if err != nil { + log.WithFields(log.Fields{ + "start": body.Start, + }).Warn(errors.Wrap(err, "Failed to parse fallback start!")) + } + end, err = strconv.ParseInt(body.End, 10, 64) + if err != nil { + log.WithFields(log.Fields{ + "end": body.End, + }).Warn(errors.Wrap(err, "Failed to parse fallback end!")) + } + text = text[:start] + text[end:] + } + } + + session.ProcessOutgoingMessage(toID, text, msg.From, replyId) return } else { toJid, err := stanza.NewJid(msg.To) -- cgit v1.2.3 From 42ed16bf9e9d72bf226045f1f291b9d58e2a6200 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 18 Mar 2023 17:43:11 -0400 Subject: Simulate carbons --- README.md | 31 +++++++++++++++++ persistence/sessions.go | 11 +++++++ telegram/commands.go | 5 +++ telegram/handlers.go | 2 +- telegram/utils.go | 74 +++++++++++++++++++++++++++++++++++++++-- xmpp/extensions/extensions.go | 77 +++++++++++++++++++++++++++++++++++++++++++ xmpp/gateway/gateway.go | 77 ++++++++++++++++++++++++++++++++++++++----- xmpp/handlers.go | 34 +++++++++++-------- 8 files changed, 284 insertions(+), 27 deletions(-) (limited to 'xmpp/extensions') diff --git a/README.md b/README.md index 0ea9f73..36aa7ca 100644 --- a/README.md +++ b/README.md @@ -142,3 +142,34 @@ server { ``` Finally, update `:upload:` in your config.yml to match `server_name` in nginx config. + +### Carbons ### + +Telegabber needs special privileges according to XEP-0356 to simulate message carbons from the users (to display messages they have sent earlier or via other clients). Example configuration for Prosody: + +``` +modules_enabled = { + [...] + + "privilege"; +} + +[...] + +Component "telegabber.yourdomain.tld" + component_secret = "yourpassword" + modules_enabled = {"privilege"} + +[...] + +VirtualHost "yourdomain.tld" + [...] + + privileged_entities = { + [...] + + ["telegabber.yourdomain.tld"] = { + message = "outgoing"; + }, + } +``` diff --git a/persistence/sessions.go b/persistence/sessions.go index ed95f60..c0750a3 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -40,6 +40,7 @@ type Session struct { RawMessages bool `yaml:":rawmessages"` AsciiArrows bool `yaml:":asciiarrows"` OOBMode bool `yaml:":oobmode"` + Carbons bool `yaml:":carbons"` } var configKeys = []string{ @@ -48,6 +49,7 @@ var configKeys = []string{ "rawmessages", "asciiarrows", "oobmode", + "carbons", } var sessionDB *SessionsYamlDB @@ -122,6 +124,8 @@ func (s *Session) Get(key string) (string, error) { return fromBool(s.AsciiArrows), nil case "oobmode": return fromBool(s.OOBMode), nil + case "carbons": + return fromBool(s.Carbons), nil } return "", errors.New("Unknown session property") @@ -172,6 +176,13 @@ func (s *Session) Set(key string, value string) (string, error) { } s.OOBMode = b return value, nil + case "carbons": + b, err := toBool(value) + if err != nil { + return "", err + } + s.Carbons = b + return value, nil } return "", errors.New("Unknown session property") diff --git a/telegram/commands.go b/telegram/commands.go index 160e486..368a461 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -193,6 +193,7 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) { strconv.FormatInt(message.Id, 10), c.xmpp, reply, + false, ) } } @@ -361,6 +362,10 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string } case "config": if len(args) > 1 { + if !gateway.MessageOutgoingPermission && args[0] == "carbons" && args[1] == "true" { + return "The server did not allow to enable carbons" + } + value, err := c.Session.Set(args[0], args[1]) if err != nil { return err.Error() diff --git a/telegram/handlers.go b/telegram/handlers.go index 307562a..84e3748 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -242,7 +242,7 @@ 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) + gateway.SendMessage(c.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 68dd524..99492e1 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -109,6 +109,33 @@ 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) { + if !c.Online() || id == 0 { + return false, errOffline + } + + var err error + + chat, ok := c.cache.GetChat(id) + if !ok { + chat, err = c.client.GetChat(&client.GetChatRequest{ + ChatId: id, + }) + if err != nil { + return false, err + } + + c.cache.SetChat(id, chat) + } + + chatType := chat.Type.ChatTypeType() + if chatType == client.TypeChatTypePrivate || chatType == client.TypeChatTypeSecret { + return true, nil + } + return false, nil +} + func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) { var show, textStatus, presenceType string @@ -782,6 +809,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) { var text, oob, auxText string + var err error reply, replyMsg := c.getMessageReply(message) @@ -847,12 +875,33 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { MessageIds: []int64{message.Id}, ForceRead: true, }) + // forward message to XMPP sId := strconv.FormatInt(message.Id, 10) sChatId := strconv.FormatInt(chatId, 10) - gateway.SendMessageWithOOB(c.jid, sChatId, text, sId, c.xmpp, reply, oob) - if auxText != "" { - gateway.SendMessage(c.jid, sChatId, auxText, sId, c.xmpp, reply) + + 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) + if auxText != "" { + gateway.SendMessage(jid, sChatId, auxText, sId, c.xmpp, reply, isOutgoing) + } } } @@ -1024,6 +1073,25 @@ func (c *Client) deleteResource(resource string) { } } +func (c *Client) resourcesRange() chan string { + c.locks.resourcesLock.Lock() + + resourceChan := make(chan string, 1) + + go func() { + defer func() { + c.locks.resourcesLock.Unlock() + close(resourceChan) + }() + + for resource := range c.resources { + resourceChan <- resource + } + }() + + return resourceChan +} + // resend statuses to (to another resource, for example) func (c *Client) roster(resource string) { if _, ok := c.resources[resource]; ok { diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index c982581..2cf7d45 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -141,6 +141,45 @@ type FallbackSubject struct { End string `xml:"end,attr"` } +// CarbonReceived is from XEP-0280 +type CarbonReceived struct { + XMLName xml.Name `xml:"urn:xmpp:carbons:2 received"` + Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` +} + +// CarbonSent is from XEP-0280 +type CarbonSent struct { + XMLName xml.Name `xml:"urn:xmpp:carbons:2 sent"` + Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` +} + +// ComponentPrivilege is from XEP-0356 +type ComponentPrivilege struct { + XMLName xml.Name `xml:"urn:xmpp:privilege:1 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"` + Access string `xml:"access,attr"` + Type string `xml:"type,attr"` + Push bool `xml:"push,attr"` +} + +// ClientMessage is a jabber:client NS message compatible with Prosody's XEP-0356 implementation +type ClientMessage struct { + XMLName xml.Name `xml:"jabber:client message"` + stanza.Attrs + + Subject string `xml:"subject,omitempty"` + Body string `xml:"body,omitempty"` + Thread string `xml:"thread,omitempty"` + Error stanza.Err `xml:"error,omitempty"` + Extensions []stanza.MsgExtension `xml:",omitempty"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -171,6 +210,26 @@ func (c Fallback) Namespace() string { return c.XMLName.Space } +// Namespace is a namespace! +func (c CarbonReceived) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c CarbonSent) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c ComponentPrivilege) Namespace() string { + return c.XMLName.Space +} + +// Name is a packet name +func (ClientMessage) Name() string { + return "message" +} + // NewReplyFallback initializes a fallback range func NewReplyFallback(start uint64, end uint64) Fallback { return Fallback{ @@ -214,4 +273,22 @@ func init() { "urn:xmpp:fallback:0", "fallback", }, Fallback{}) + + // carbon received + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:carbons:2", + "received", + }, CarbonReceived{}) + + // carbon sent + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:carbons:2", + "sent", + }, CarbonSent{}) + + // component privilege + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:privilege:1", + "privilege", + }, ComponentPrivilege{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index de8b495..534ee7e 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -2,6 +2,7 @@ package gateway import ( "encoding/xml" + "github.com/pkg/errors" "strings" "sync" @@ -33,31 +34,44 @@ var Jid *stanza.Jid // 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 + // SendMessage creates and sends a message stanza -func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply) { - sendMessageWrapper(to, from, body, id, component, reply, "") +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) } // SendServiceMessage creates and sends a simple message stanza from transport func SendServiceMessage(to string, body string, component *xmpp.Component) { - sendMessageWrapper(to, "", body, "", component, nil, "") + sendMessageWrapper(to, "", body, "", component, 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, "") + sendMessageWrapper(to, from, body, "", component, 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) { - sendMessageWrapper(to, from, body, id, component, reply, oob) +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 sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string) { +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isOutgoing bool) { + toJid, err := stanza.NewJid(to) + if err != nil { + log.WithFields(log.Fields{ + "to": to, + }).Error(errors.Wrap(err, "Invalid to JID!")) + return + } + bareTo := toJid.Bare() + componentJid := Jid.Full() var logFrom string var messageFrom string + var messageTo string if from == "" { logFrom = componentJid messageFrom = componentJid @@ -65,6 +79,12 @@ func sendMessageWrapper(to string, from string, body string, id string, componen logFrom = from messageFrom = from + "@" + componentJid } + if isOutgoing { + messageTo = messageFrom + messageFrom = bareTo + "/" + Jid.Resource + } else { + messageTo = to + } log.WithFields(log.Fields{ "from": logFrom, @@ -74,7 +94,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen message := stanza.Message{ Attrs: stanza.Attrs{ From: messageFrom, - To: to, + To: messageTo, Type: "chat", Id: id, }, @@ -96,7 +116,34 @@ func sendMessageWrapper(to string, from string, body string, id string, componen } } - sendMessage(&message, component) + if isOutgoing { + carbonMessage := extensions.ClientMessage{ + Attrs: stanza.Attrs{ + From: bareTo, + To: to, + Type: "chat", + }, + } + carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{ + Forwarded: stanza.Forwarded{ + Stanza: extensions.ClientMessage(message), + }, + }) + privilegeMessage := stanza.Message{ + Attrs: stanza.Attrs{ + From: Jid.Bare(), + To: toJid.Domain, + }, + } + privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege{ + Forwarded: stanza.Forwarded{ + Stanza: carbonMessage, + }, + }) + sendMessage(&privilegeMessage, component) + } else { + sendMessage(&message, component) + } } // SetNickname sets a new nickname for a contact @@ -297,3 +344,15 @@ func ResumableSend(component *xmpp.Component, packet stanza.Packet) error { } return err } + +// SplitJID tokenizes a JID string to bare JID and resource +func SplitJID(from string) (string, string, bool) { + fromJid, err := stanza.NewJid(from) + if err != nil { + log.WithFields(log.Fields{ + "from": from, + }).Error(errors.Wrap(err, "Invalid from JID!")) + return "", "", false + } + return fromJid.Bare(), fromJid.Resource, true +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index acf62a2..2bdbaef 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -79,7 +79,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { }).Warn("Message") log.Debugf("%#v", msg) - bare, resource, ok := splitFrom(msg.From) + bare, resource, ok := gateway.SplitJID(msg.From) if !ok { return } @@ -152,6 +152,23 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { } log.Warn("Unknown purpose of the message, skipping") } + + if msg.Body == "" { + var privilege extensions.ComponentPrivilege + if ok := msg.Get(&privilege); ok { + log.Debugf("privilege: %#v", privilege) + } + + for _, perm := range privilege.Perms { + if perm.Access == "message" && perm.Type == "outgoing" { + gateway.MessageOutgoingPermission = true + } + } + } + + if msg.Type == "error" { + log.Errorf("MESSAGE ERROR: %#v", p) + } } // HandlePresence processes an incoming XMPP presence @@ -196,7 +213,7 @@ func handleSubscription(s xmpp.Sender, p stanza.Presence) { if !ok { return } - bare, _, ok := splitFrom(p.From) + bare, _, ok := gateway.SplitJID(p.From) if !ok { return } @@ -227,7 +244,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { log.Debugf("%#v", p) // create session - bare, resource, ok := splitFrom(p.From) + bare, resource, ok := gateway.SplitJID(p.From) if !ok { return } @@ -385,17 +402,6 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { _ = gateway.ResumableSend(component, answer) } -func splitFrom(from string) (string, string, bool) { - fromJid, err := stanza.NewJid(from) - if err != nil { - log.WithFields(log.Fields{ - "from": from, - }).Error(errors.Wrap(err, "Invalid from JID!")) - return "", "", false - } - return fromJid.Bare(), fromJid.Resource, true -} - func toToID(to string) (int64, bool) { toParts := strings.Split(to, "@") if len(toParts) < 2 { -- cgit v1.2.3 From 22b46c71ce932cc97fa6d437a7473a653bb59a41 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Sat, 18 Mar 2023 17:47:08 -0400 Subject: gofmt --- telegram/commands.go | 8 ++++---- telegram/handlers.go | 2 +- telegram/utils.go | 8 ++++---- xmpp/extensions/extensions.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) (limited to 'xmpp/extensions') diff --git a/telegram/commands.go b/telegram/commands.go index 368a461..2a72219 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -52,10 +52,10 @@ var transportCommands = map[string]command{ "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»"}, + "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»"}, } var chatCommands = map[string]command{ diff --git a/telegram/handlers.go b/telegram/handlers.go index 84e3748..bd768ae 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -242,7 +242,7 @@ 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) + gateway.SendMessage(c.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 99492e1..905a97b 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -320,7 +320,7 @@ func (c *Client) getMessageReply(message *client.Message) (reply *gateway.Reply, return } - reply = &gateway.Reply { + reply = &gateway.Reply{ Author: fmt.Sprintf("%v@%s", c.getSenderId(replyMsg), gateway.Jid.Full()), Id: strconv.FormatInt(message.ReplyToMessageId, 10), } @@ -770,8 +770,8 @@ func (c *Client) messageToPrefix(message *client.Message, previewString string, } // reply to if message.ReplyToMessageId != 0 { - replyStart = c.countCharsInLines(&prefix) + (len(prefix) - 1) * len(messageHeaderSeparator) - replyLine := "reply: "+c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg) + replyStart = c.countCharsInLines(&prefix) + (len(prefix)-1)*len(messageHeaderSeparator) + replyLine := "reply: " + c.formatMessage(message.ChatId, message.ReplyToMessageId, true, replyMsg) prefix = append(prefix, replyLine) replyEnd = replyStart + len(replyLine) + len(messageHeaderSeparator) } @@ -891,7 +891,7 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { isOutgoing := isPM && message.IsOutgoing if isOutgoing { for resource := range c.resourcesRange() { - jids = append(jids, c.jid + "/" + resource) + jids = append(jids, c.jid+"/"+resource) } } else { jids = []string{c.jid} diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index 2cf7d45..78de47d 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -237,7 +237,7 @@ func NewReplyFallback(start uint64, end uint64) Fallback { Body: []FallbackBody{ FallbackBody{ Start: strconv.FormatUint(start, 10), - End: strconv.FormatUint(end, 10), + End: strconv.FormatUint(end, 10), }, }, } -- cgit v1.2.3 From 7215d11d7973b9896c6223938649c75165fa3ae7 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 5 Jun 2023 04:22:13 -0400 Subject: 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(-) (limited to 'xmpp/extensions') 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) -- cgit v1.2.3 From c03ccfdfb713d4fcb089600d9fd91f03e469daca Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 2 Aug 2023 17:08:06 -0400 Subject: 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(-) (limited to 'xmpp/extensions') 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 } } } -- cgit v1.2.3 From 20994e29953dfc9c238f69d919912e0c26e36b97 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Mon, 28 Aug 2023 10:16:57 -0400 Subject: In-Band Registration (XEP-0077) --- telegram/commands.go | 52 +++++------ telegram/connect.go | 40 ++++++++- xmpp/extensions/extensions.go | 36 ++++++++ xmpp/gateway/gateway.go | 6 ++ xmpp/handlers.go | 204 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 303 insertions(+), 35 deletions(-) (limited to 'xmpp/extensions') 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 - } + c.authorizer.PhoneNumber <- args[0] + } else { + if c.authorizer == nil { + return TelegramNotInitialized + } - if c.authorizer.isClosed { - return "Authorization is done already" - } + if c.authorizer.isClosed { + return TelegramAuthDone + } - 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] + 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 { -- cgit v1.2.3