aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md31
-rw-r--r--persistence/sessions.go11
-rw-r--r--telegram/commands.go5
-rw-r--r--telegram/handlers.go2
-rw-r--r--telegram/utils.go74
-rw-r--r--xmpp/extensions/extensions.go77
-rw-r--r--xmpp/gateway/gateway.go77
-rw-r--r--xmpp/handlers.go34
8 files changed, 284 insertions, 27 deletions
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 {