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 {