Simulate carbons

This commit is contained in:
Bohdan Horbeshko 2023-03-18 17:43:11 -04:00
parent 90807b2d9e
commit 42ed16bf9e
8 changed files with 284 additions and 27 deletions

View file

@ -142,3 +142,34 @@ server {
``` ```
Finally, update `:upload:` in your config.yml to match `server_name` in nginx config. 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";
},
}
```

View file

@ -40,6 +40,7 @@ type Session struct {
RawMessages bool `yaml:":rawmessages"` RawMessages bool `yaml:":rawmessages"`
AsciiArrows bool `yaml:":asciiarrows"` AsciiArrows bool `yaml:":asciiarrows"`
OOBMode bool `yaml:":oobmode"` OOBMode bool `yaml:":oobmode"`
Carbons bool `yaml:":carbons"`
} }
var configKeys = []string{ var configKeys = []string{
@ -48,6 +49,7 @@ var configKeys = []string{
"rawmessages", "rawmessages",
"asciiarrows", "asciiarrows",
"oobmode", "oobmode",
"carbons",
} }
var sessionDB *SessionsYamlDB var sessionDB *SessionsYamlDB
@ -122,6 +124,8 @@ func (s *Session) Get(key string) (string, error) {
return fromBool(s.AsciiArrows), nil return fromBool(s.AsciiArrows), nil
case "oobmode": case "oobmode":
return fromBool(s.OOBMode), nil return fromBool(s.OOBMode), nil
case "carbons":
return fromBool(s.Carbons), nil
} }
return "", errors.New("Unknown session property") return "", errors.New("Unknown session property")
@ -172,6 +176,13 @@ func (s *Session) Set(key string, value string) (string, error) {
} }
s.OOBMode = b s.OOBMode = b
return value, nil 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") return "", errors.New("Unknown session property")

View file

@ -193,6 +193,7 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message) {
strconv.FormatInt(message.Id, 10), strconv.FormatInt(message.Id, 10),
c.xmpp, c.xmpp,
reply, reply,
false,
) )
} }
} }
@ -361,6 +362,10 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
case "config": case "config":
if len(args) > 1 { 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]) value, err := c.Session.Set(args[0], args[1])
if err != nil { if err != nil {
return err.Error() return err.Error()

View file

@ -242,7 +242,7 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
textContent.Text.Entities, textContent.Text.Entities,
markupFunction, 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)
} }
} }

View file

@ -109,6 +109,33 @@ func (c *Client) GetContactByID(id int64, chat *client.Chat) (*client.Chat, *cli
return chat, user, nil 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) { func (c *Client) userStatusToText(status client.UserStatus, chatID int64) (string, string, string) {
var show, textStatus, presenceType 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 // ProcessIncomingMessage transfers a message to XMPP side and marks it as read on Telegram side
func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) { func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
var text, oob, auxText string var text, oob, auxText string
var err error
reply, replyMsg := c.getMessageReply(message) reply, replyMsg := c.getMessageReply(message)
@ -847,12 +875,33 @@ func (c *Client) ProcessIncomingMessage(chatId int64, message *client.Message) {
MessageIds: []int64{message.Id}, MessageIds: []int64{message.Id},
ForceRead: true, ForceRead: true,
}) })
// forward message to XMPP // forward message to XMPP
sId := strconv.FormatInt(message.Id, 10) sId := strconv.FormatInt(message.Id, 10)
sChatId := strconv.FormatInt(chatId, 10) sChatId := strconv.FormatInt(chatId, 10)
gateway.SendMessageWithOOB(c.jid, sChatId, text, sId, c.xmpp, reply, oob)
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 != "" { if auxText != "" {
gateway.SendMessage(c.jid, sChatId, auxText, sId, c.xmpp, reply) 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) // resend statuses to (to another resource, for example)
func (c *Client) roster(resource string) { func (c *Client) roster(resource string) {
if _, ok := c.resources[resource]; ok { if _, ok := c.resources[resource]; ok {

View file

@ -141,6 +141,45 @@ type FallbackSubject struct {
End string `xml:"end,attr"` 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! // Namespace is a namespace!
func (c PresenceNickExtension) Namespace() string { func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space return c.XMLName.Space
@ -171,6 +210,26 @@ func (c Fallback) Namespace() string {
return c.XMLName.Space 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 // NewReplyFallback initializes a fallback range
func NewReplyFallback(start uint64, end uint64) Fallback { func NewReplyFallback(start uint64, end uint64) Fallback {
return Fallback{ return Fallback{
@ -214,4 +273,22 @@ func init() {
"urn:xmpp:fallback:0", "urn:xmpp:fallback:0",
"fallback", "fallback",
}, 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{})
} }

View file

@ -2,6 +2,7 @@ package gateway
import ( import (
"encoding/xml" "encoding/xml"
"github.com/pkg/errors"
"strings" "strings"
"sync" "sync"
@ -33,31 +34,44 @@ var Jid *stanza.Jid
// were changed and need to be re-flushed to the YamlDB // were changed and need to be re-flushed to the YamlDB
var DirtySessions = false var DirtySessions = false
// MessageOutgoingPermission allows to fake outgoing messages by foreign JIDs
var MessageOutgoingPermission = false
// SendMessage creates and sends a message stanza // SendMessage creates and sends a message stanza
func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *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, "") sendMessageWrapper(to, from, body, id, component, reply, "", isOutgoing)
} }
// SendServiceMessage creates and sends a simple message stanza from transport // SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to string, body string, component *xmpp.Component) { 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 // SendTextMessage creates and sends a simple message stanza
func SendTextMessage(to string, from string, body string, component *xmpp.Component) { 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 // 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) { 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) 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() componentJid := Jid.Full()
var logFrom string var logFrom string
var messageFrom string var messageFrom string
var messageTo string
if from == "" { if from == "" {
logFrom = componentJid logFrom = componentJid
messageFrom = componentJid messageFrom = componentJid
@ -65,6 +79,12 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
logFrom = from logFrom = from
messageFrom = from + "@" + componentJid messageFrom = from + "@" + componentJid
} }
if isOutgoing {
messageTo = messageFrom
messageFrom = bareTo + "/" + Jid.Resource
} else {
messageTo = to
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"from": logFrom, "from": logFrom,
@ -74,7 +94,7 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
message := stanza.Message{ message := stanza.Message{
Attrs: stanza.Attrs{ Attrs: stanza.Attrs{
From: messageFrom, From: messageFrom,
To: to, To: messageTo,
Type: "chat", Type: "chat",
Id: id, Id: id,
}, },
@ -96,8 +116,35 @@ func sendMessageWrapper(to string, from string, body string, id string, componen
} }
} }
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) sendMessage(&message, component)
} }
}
// SetNickname sets a new nickname for a contact // SetNickname sets a new nickname for a contact
func SetNickname(to string, from string, nickname string, component *xmpp.Component) { func SetNickname(to string, from string, nickname string, component *xmpp.Component) {
@ -297,3 +344,15 @@ func ResumableSend(component *xmpp.Component, packet stanza.Packet) error {
} }
return err 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
}

View file

@ -79,7 +79,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
}).Warn("Message") }).Warn("Message")
log.Debugf("%#v", msg) log.Debugf("%#v", msg)
bare, resource, ok := splitFrom(msg.From) bare, resource, ok := gateway.SplitJID(msg.From)
if !ok { if !ok {
return return
} }
@ -152,6 +152,23 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
} }
log.Warn("Unknown purpose of the message, skipping") 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 // HandlePresence processes an incoming XMPP presence
@ -196,7 +213,7 @@ func handleSubscription(s xmpp.Sender, p stanza.Presence) {
if !ok { if !ok {
return return
} }
bare, _, ok := splitFrom(p.From) bare, _, ok := gateway.SplitJID(p.From)
if !ok { if !ok {
return return
} }
@ -227,7 +244,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) {
log.Debugf("%#v", p) log.Debugf("%#v", p)
// create session // create session
bare, resource, ok := splitFrom(p.From) bare, resource, ok := gateway.SplitJID(p.From)
if !ok { if !ok {
return return
} }
@ -385,17 +402,6 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
_ = gateway.ResumableSend(component, answer) _ = 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) { func toToID(to string) (int64, bool) {
toParts := strings.Split(to, "@") toParts := strings.Split(to, "@")
if len(toParts) < 2 { if len(toParts) < 2 {