You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
telegabber/xmpp/gateway/gateway.go

491 lines
11 KiB

package gateway
import (
"bytes"
"encoding/base64"
"encoding/xml"
"github.com/pkg/errors"
"fmt"
"io"
"sort"
"strings"
"sync"
"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
}
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
// DirtySessions denotes that some Telegram session configurations
// 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
// CapsType is a capability category
type CapsType int
const (
CapsAudio CapsType = iota
)
// ContactType is a disco JID category
type ContactType int
const (
ContactTransport CapsType = iota
ContactPM
)
// SendMessage creates and sends a message stanza
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, "", 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, "", 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, 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, 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
} else {
logFrom = from
messageFrom = from + "@" + componentJid
}
if isOutgoing {
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 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
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))
// SPCaps is a XEP-0115 verification string
var SPCaps = args.NewString()
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,
},
})
}
}
if SPCaps.IsSet(args) {
ver := SPCaps.Get(args)
if ver != "" {
presence.Extensions = append(presence.Extensions, extensions.CapsExtension{
Hash: "sha-1",
Node: "https://dev.narayana.im/narayana/telegabber/",
Ver: ver,
})
}
}
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
}
// 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
}
// 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
}
func getDiscoFeatures(caps []CapsType) []string {
features := []string{
"http://jabber.org/protocol/caps",
"http://jabber.org/protocol/disco#info",
}
for typ := range features {
switch typ {
case CapsAudio:
features = append(
features,
"urn:xmpp:jingle-message:0",
"urn:xmpp:jingle:1",
"urn:xmpp:jingle:apps:dtls:0",
"urn:xmpp:jingle:apps:rtp:1",
"urn:xmpp:jingle:apps:rtp:audio",
"urn:xmpp:jingle:transports:ice-udp:1",
)
}
}
return features
}
// GetDiscoInfo generates a disco info IQ query response
func GetDiscoInfo(typ ContactType, features []string) *stanza.DiscoInfo {
disco := stanza.DiscoInfo{}
if typ == ContactPM {
disco.AddIdentity("", "account", "registered")
} else {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
}
disco.AddFeatures(features...)
return &disco
}
// GetCapsVer hashes a capabilities set into a verification string
func GetCapsVer(caps []CapsType) (string, error) {
features := getDiscoFeatures(caps)
disco := GetDiscoInfo(features)
discoToCapsHash(disco)
buf := new(bytes.Buffer)
binval := base64.NewEncoder(base64.StdEncoding, buf)
_, err = io.Copy(binval, file)
binval.Close()
if err != nil {
return "", errors.Wrap(err, "Error calculating caps base64")
}
return buf.String(), nil
}
func iOctetComparator(a, b string) bool {
return a < b
}
func discoToCaps(disco *stanza.DiscoInfo) string {
var s strings.Builder
var identities, vars, capsForms []string
for _, identity := range disco.Identity {
identities = append(identities, fmt.Sprintf(
"%s/%s//%s",
identity.Category,
identity.Type,
identity.Name,
))
}
sort.Slice(identities, iOctetComparator)
for _, identity := range identities {
s.WriteString(identity)
s.WriteString(">")
}
for _, feature := range disco.Features {
vars = append(vars, feature.Var)
}
sort.Slice(vars, iOctetComparator)
for _, var := range vars {
s.WriteString(var)
s.WriteString(">")
}
if disco.Form != nil {
fields := make([]*stanza.Field, len(disco.Form.Fields))
copy(fields, disco.Form.Fields)
sort.Slice(fields, func(a, b *stanza.Field) bool {
if a.Var == "FORM_TYPE" {
return true
}
if b.Var == "FORM_TYPE" {
return false
}
return a.Var < b.Var
})
for _, field := range fields {
}
}
return s.String()
}