491 lines
11 KiB
Go
491 lines
11 KiB
Go
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()
|
|
}
|