telegabber/xmpp/gateway/gateway.go

428 lines
11 KiB
Go

package gateway
import (
"encoding/xml"
"github.com/pkg/errors"
"strconv"
"strings"
"sync"
"dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
log "github.com/sirupsen/logrus"
"github.com/soheilhy/args"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
type Reply struct {
Author string
Id string
Start uint64
End uint64
}
type MarkerType byte
const (
MarkerTypeReceived MarkerType = iota
MarkerTypeDisplayed
)
type marker struct {
Type MarkerType
Id string
}
const NSNick string = "http://jabber.org/protocol/nick"
// Queue stores presences to send later
var Queue = make(map[string]*stanza.Presence)
var QueueLock = sync.Mutex{}
// Jid stores the component's JID object
var Jid *stanza.Jid
// IdsDB provides a disk-backed bidirectional dictionary of Telegram and XMPP ids
var IdsDB badger.IdsDB
// DirtySessions denotes that some Telegram session configurations
// were changed and need to be re-flushed to the YamlDB
var DirtySessions = false
// 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, replaceId string, isCarbon, requestReceipt bool) {
sendMessageWrapper(to, from, body, id, component, reply, nil, "", replaceId, isCarbon, requestReceipt)
}
// SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to string, body string, component *xmpp.Component) {
sendMessageWrapper(to, "", body, "", component, nil, nil, "", "", false, false)
}
// SendTextMessage creates and sends a simple message stanza
func SendTextMessage(to string, from string, body string, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", component, nil, nil, "", "", false, false)
}
// 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, replaceId string, isCarbon, requestReceipt bool) {
sendMessageWrapper(to, from, body, id, component, reply, nil, oob, replaceId, isCarbon, requestReceipt)
}
// SendMessageMarker creates and sends a message stanza with a XEP-0333 marker
func SendMessageMarker(to string, from string, component *xmpp.Component, markerType MarkerType, markerId string) {
sendMessageWrapper(to, from, "", "", component, nil, &marker{
Type: markerType,
Id: markerId,
}, "", "", false, false)
}
func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob, replaceId string, isCarbon, requestReceipt bool) {
toJid, err := stanza.NewJid(to)
if err != nil {
log.WithFields(log.Fields{
"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
} else {
logFrom = from
messageFrom = from + "@" + componentJid
}
if isCarbon {
messageTo = messageFrom
messageFrom = bareTo + "/" + Jid.Resource
} else {
messageTo = to
}
log.WithFields(log.Fields{
"from": logFrom,
"to": to,
}).Warn("Got message")
message := stanza.Message{
Attrs: stanza.Attrs{
From: messageFrom,
To: messageTo,
Type: "chat",
Id: id,
},
Body: body,
}
if oob != "" {
message.Extensions = append(message.Extensions, stanza.OOB{
URL: oob,
})
}
if reply != nil {
message.Extensions = append(message.Extensions, extensions.Reply{
To: reply.Author,
Id: reply.Id,
})
if reply.End > 0 {
message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End))
}
}
if marker != nil {
if marker.Type == MarkerTypeReceived {
message.Extensions = append(message.Extensions, stanza.MarkReceived{ID: marker.Id})
} else if marker.Type == MarkerTypeDisplayed {
message.Extensions = append(message.Extensions, stanza.MarkDisplayed{ID: marker.Id})
message.Extensions = append(message.Extensions, stanza.ReceiptReceived{ID: marker.Id})
}
}
if !isCarbon && toJid.Resource != "" {
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
}
if requestReceipt {
message.Extensions = append(message.Extensions, stanza.Markable{})
}
if replaceId != "" {
message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId})
}
if isCarbon {
carbonMessage := extensions.ClientMessage{
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,
},
}
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)
}
}
// SetNickname sets a new nickname for a contact
func SetNickname(to string, from string, nickname string, component *xmpp.Component) {
componentJid := Jid.Bare()
messageFrom := from + "@" + componentJid
log.WithFields(log.Fields{
"from": from,
"to": to,
}).Warn("Set nickname")
message := stanza.Message{
Attrs: stanza.Attrs{
From: messageFrom,
To: to,
Type: "headline",
},
Extensions: []stanza.MsgExtension{
stanza.PubSubEvent{
EventElement: stanza.ItemsEvent{
Node: NSNick,
Items: []stanza.ItemEvent{
stanza.ItemEvent{
Any: &stanza.Node{
XMLName: xml.Name{Space: NSNick, Local: "nick"},
Content: nickname,
},
},
},
},
},
},
}
sendMessage(&message, component)
}
func sendMessage(message *stanza.Message, component *xmpp.Component) {
// explicit check, as marshalling is expensive
if log.GetLevel() == log.DebugLevel {
xmlMessage, err := xml.Marshal(message)
if err == nil {
log.Debug(string(xmlMessage))
} else {
log.Debugf("%#v", message)
}
}
_ = ResumableSend(component, message)
}
// LogBadPresence verbosely logs a presence
func LogBadPresence(presence *stanza.Presence) {
log.Errorf("Couldn't send presence: %#v", presence)
}
// SPFrom is a Telegram user id
var SPFrom = args.NewString()
// SPType is a presence type
var SPType = args.NewString()
// SPShow is a availability status
var SPShow = args.NewString()
// SPStatus is a verbose status
var SPStatus = args.NewString()
// SPNickname is a XEP-0172 nickname
var SPNickname = args.NewString()
// SPPhoto is a XEP-0153 hash of avatar in vCard
var SPPhoto = args.NewString()
// SPResource is an optional resource
var SPResource = args.NewString()
// SPImmed skips queueing
var SPImmed = args.NewBool(args.Default(true))
func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
var presenceFrom string
if SPFrom.IsSet(args) {
presenceFrom = SPFrom.Get(args) + "@" + bareJid
if SPResource.IsSet(args) {
resource := SPResource.Get(args)
if resource != "" {
presenceFrom += "/" + resource
}
}
} else {
presenceFrom = bareJid
}
presence := stanza.Presence{Attrs: stanza.Attrs{
From: presenceFrom,
To: to,
}}
if SPType.IsSet(args) {
t := SPType.Get(args)
if t != "" {
presence.Attrs.Type = stanza.StanzaType(t)
}
}
if SPShow.IsSet(args) {
show := SPShow.Get(args)
if show != "" {
presence.Show = stanza.PresenceShow(show)
}
}
if SPStatus.IsSet(args) {
status := SPStatus.Get(args)
if status != "" {
presence.Status = status
}
}
if SPNickname.IsSet(args) {
nickname := SPNickname.Get(args)
if nickname != "" {
presence.Extensions = append(presence.Extensions, extensions.PresenceNickExtension{
Text: nickname,
})
}
}
if SPPhoto.IsSet(args) {
photo := SPPhoto.Get(args)
if photo != "" {
presence.Extensions = append(presence.Extensions, extensions.PresenceXVCardUpdateExtension{
Photo: extensions.PresenceXVCardUpdatePhoto{
Text: photo,
},
})
}
}
return presence
}
// SendPresence creates and sends a presence stanza
func SendPresence(component *xmpp.Component, to string, args ...args.V) error {
var logFrom string
bareJid := Jid.Bare()
if SPFrom.IsSet(args) {
logFrom = SPFrom.Get(args)
} else {
logFrom = bareJid
}
log.WithFields(log.Fields{
"type": SPType.Get(args),
"from": logFrom,
"to": to,
}).Info("Got presence")
presence := newPresence(bareJid, to, args...)
// explicit check, as marshalling is expensive
if log.GetLevel() == log.DebugLevel {
xmlPresence, err := xml.Marshal(presence)
if err == nil {
log.Debug(string(xmlPresence))
} else {
log.Debugf("%#v", presence)
}
}
immed := SPImmed.Get(args)
if immed {
err := ResumableSend(component, presence)
if err != nil {
LogBadPresence(&presence)
return err
}
} else {
QueueLock.Lock()
Queue[presence.From+presence.To] = &presence
QueueLock.Unlock()
}
return nil
}
// SPAppendFrom appends numeric from and resource to varargs
func SPAppendFrom(oldArgs []args.V, id int64) []args.V {
newArgs := append(oldArgs, SPFrom(strconv.FormatInt(id, 10)))
newArgs = append(newArgs, SPResource(Jid.Resource))
return newArgs
}
// SimplePresence crafts simple presence varargs
func SimplePresence(from int64, typ string) []args.V {
args := []args.V{SPType(typ)}
args = SPAppendFrom(args, from)
return args
}
// ResumableSend tries to resume the connection once and sends the packet again
func ResumableSend(component *xmpp.Component, packet stanza.Packet) error {
err := component.Send(packet)
if err != nil && strings.HasPrefix(err.Error(), "cannot send packet") {
log.Warn("Packet send failed, trying to resume the connection...")
err = component.Connect()
if err == nil {
err = component.Send(packet)
}
}
if err != nil {
log.Error(err.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)
if err != nil {
log.WithFields(log.Fields{
"from": from,
}).Error(errors.Wrap(err, "Invalid from JID!"))
return "", "", false
}
return fromJid.Bare(), fromJid.Resource, true
}