580 lines
12 KiB
Go
580 lines
12 KiB
Go
package xmpp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"github.com/pkg/errors"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"dev.narayana.im/narayana/telegabber/persistence"
|
|
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
|
|
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"gosrc.io/xmpp"
|
|
"gosrc.io/xmpp/stanza"
|
|
)
|
|
|
|
const (
|
|
TypeVCardTemp byte = iota
|
|
TypeVCard4
|
|
)
|
|
const NodeVCard4 string = "urn:xmpp:vcard4"
|
|
|
|
type discoType int
|
|
const (
|
|
discoTypeInfo discoType = iota
|
|
discoTypeItems
|
|
)
|
|
|
|
func logPacketType(p stanza.Packet) {
|
|
log.Warnf("Ignoring packet: %T\n", p)
|
|
}
|
|
|
|
// HandleIq processes an incoming XMPP iq
|
|
func HandleIq(s xmpp.Sender, p stanza.Packet) {
|
|
iq, ok := p.(*stanza.IQ)
|
|
if !ok {
|
|
logPacketType(p)
|
|
return
|
|
}
|
|
|
|
log.Debugf("%#v", iq)
|
|
if iq.Type == "get" {
|
|
_, ok := iq.Payload.(*extensions.IqVcardTemp)
|
|
if ok {
|
|
go handleGetVcardIq(s, iq, TypeVCardTemp)
|
|
return
|
|
}
|
|
pubsub, ok := iq.Payload.(*stanza.PubSubGeneric)
|
|
if ok {
|
|
if pubsub.Items != nil && pubsub.Items.Node == NodeVCard4 {
|
|
go handleGetVcardIq(s, iq, TypeVCard4)
|
|
return
|
|
}
|
|
}
|
|
_, ok = iq.Payload.(*stanza.DiscoInfo)
|
|
if ok {
|
|
go handleGetDisco(discoTypeInfo, s, iq)
|
|
return
|
|
}
|
|
_, ok = iq.Payload.(*stanza.DiscoItems)
|
|
if ok {
|
|
go handleGetDisco(discoTypeItems, s, iq)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// HandleMessage processes an incoming XMPP message
|
|
func HandleMessage(s xmpp.Sender, p stanza.Packet) {
|
|
msg, ok := p.(stanza.Message)
|
|
if !ok {
|
|
logPacketType(p)
|
|
return
|
|
}
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
if msg.Type != "error" && msg.Body != "" {
|
|
log.WithFields(log.Fields{
|
|
"from": msg.From,
|
|
"to": msg.To,
|
|
}).Warn("Message")
|
|
log.Debugf("%#v", msg)
|
|
|
|
bare, resource, ok := splitFrom(msg.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
gatewayJid := gateway.Jid.Bare()
|
|
|
|
session, ok := sessions[bare]
|
|
if !ok {
|
|
if msg.To == gatewayJid {
|
|
gateway.SendPresence(component, msg.From, gateway.SPType("subscribe"))
|
|
gateway.SendPresence(component, msg.From, gateway.SPType("subscribed"))
|
|
} else {
|
|
log.Error("Message from stranger")
|
|
return
|
|
}
|
|
}
|
|
|
|
toID, ok := toToID(msg.To)
|
|
if ok {
|
|
session.ProcessOutgoingMessage(toID, msg.Body, msg.From)
|
|
return
|
|
} else {
|
|
toJid, err := stanza.NewJid(msg.To)
|
|
if err == nil && toJid.Bare() == gatewayJid && (strings.HasPrefix(msg.Body, "/") || strings.HasPrefix(msg.Body, "!")) {
|
|
response := session.ProcessTransportCommand(msg.Body, resource)
|
|
if response != "" {
|
|
gateway.SendMessage(msg.From, "", response, component)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
log.Warn("Unknown purpose of the message, skipping")
|
|
}
|
|
}
|
|
|
|
// HandlePresence processes an incoming XMPP presence
|
|
func HandlePresence(s xmpp.Sender, p stanza.Packet) {
|
|
prs, ok := p.(stanza.Presence)
|
|
if !ok {
|
|
logPacketType(p)
|
|
return
|
|
}
|
|
|
|
if prs.Type == "subscribe" {
|
|
handleSubscription(s, prs)
|
|
}
|
|
if prs.To == gateway.Jid.Bare() {
|
|
handlePresence(s, prs)
|
|
}
|
|
}
|
|
|
|
func handleSubscription(s xmpp.Sender, p stanza.Presence) {
|
|
log.WithFields(log.Fields{
|
|
"from": p.From,
|
|
"to": p.To,
|
|
}).Warn("Subscription request")
|
|
log.Debugf("%#v", p)
|
|
|
|
reply := stanza.Presence{Attrs: stanza.Attrs{
|
|
From: p.To,
|
|
To: p.From,
|
|
Id: p.Id,
|
|
Type: "subscribed",
|
|
}}
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
_ = gateway.ResumableSend(component, reply)
|
|
|
|
toID, ok := toToID(p.To)
|
|
if !ok {
|
|
return
|
|
}
|
|
bare, _, ok := splitFrom(p.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
session, ok := getTelegramInstance(bare, &persistence.Session{}, component)
|
|
if !ok {
|
|
return
|
|
}
|
|
go session.ProcessStatusUpdate(toID, "", "", gateway.SPImmed(false))
|
|
}
|
|
|
|
func handlePresence(s xmpp.Sender, p stanza.Presence) {
|
|
presenceType := p.Type
|
|
if presenceType == "" {
|
|
presenceType = "online"
|
|
}
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
log.WithFields(log.Fields{
|
|
"type": presenceType,
|
|
"from": p.From,
|
|
"to": p.To,
|
|
}).Warn("Presence")
|
|
log.Debugf("%#v", p)
|
|
|
|
// create session
|
|
bare, resource, ok := splitFrom(p.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
session, ok := getTelegramInstance(bare, &persistence.Session{}, component)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
switch p.Type {
|
|
// destroy session
|
|
case "unsubscribed", "unsubscribe":
|
|
if session.Disconnect(resource, false) {
|
|
sessionLock.Lock()
|
|
delete(sessions, bare)
|
|
sessionLock.Unlock()
|
|
}
|
|
// go offline
|
|
case "unavailable", "error":
|
|
session.Disconnect(resource, false)
|
|
// go online
|
|
case "probe", "", "online", "subscribe":
|
|
// due to the weird implementation of go-tdlib wrapper, it won't
|
|
// return the client instance until successful authorization
|
|
go func() {
|
|
err := session.Connect(resource)
|
|
if err != nil {
|
|
log.Error(errors.Wrap(err, "TDlib connection failure"))
|
|
} else {
|
|
for status := range session.StatusesRange() {
|
|
go session.ProcessStatusUpdate(
|
|
status.ID,
|
|
status.Description,
|
|
status.XMPP,
|
|
gateway.SPImmed(false),
|
|
)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
|
|
log.WithFields(log.Fields{
|
|
"from": iq.From,
|
|
"to": iq.To,
|
|
}).Warn("VCard request")
|
|
|
|
fromJid, err := stanza.NewJid(iq.From)
|
|
if err != nil {
|
|
log.Error("Invalid from JID!")
|
|
return
|
|
}
|
|
|
|
session, ok := sessions[fromJid.Bare()]
|
|
if !ok {
|
|
log.Error("IQ from stranger")
|
|
return
|
|
}
|
|
|
|
toParts := strings.Split(iq.To, "@")
|
|
toID, err := strconv.ParseInt(toParts[0], 10, 64)
|
|
if err != nil {
|
|
log.Error("Invalid IQ to")
|
|
return
|
|
}
|
|
chat, user, err := session.GetContactByID(toID, nil)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
|
|
var fn, photo, nickname, given, family, tel, info string
|
|
if chat != nil {
|
|
fn = chat.Title
|
|
|
|
if chat.Photo != nil {
|
|
file, path, err := session.OpenPhotoFile(chat.Photo.Small, 32)
|
|
if err == nil {
|
|
defer file.Close()
|
|
|
|
buf := new(bytes.Buffer)
|
|
binval := base64.NewEncoder(base64.StdEncoding, buf)
|
|
_, err = io.Copy(binval, file)
|
|
binval.Close()
|
|
if err == nil {
|
|
photo = buf.String()
|
|
} else {
|
|
log.Errorf("Error calculating base64: %v", path)
|
|
}
|
|
} else if path != "" {
|
|
log.Errorf("Photo does not exist: %v", path)
|
|
} else {
|
|
log.Errorf("PHOTO: %#v", err.Error())
|
|
}
|
|
}
|
|
info = session.GetChatDescription(chat)
|
|
}
|
|
if user != nil {
|
|
nickname = user.Username
|
|
given = user.FirstName
|
|
family = user.LastName
|
|
tel = user.PhoneNumber
|
|
}
|
|
|
|
answer := stanza.IQ{
|
|
Attrs: stanza.Attrs{
|
|
From: iq.To,
|
|
To: iq.From,
|
|
Id: iq.Id,
|
|
Type: "result",
|
|
},
|
|
Payload: makeVCardPayload(typ, iq.To, fn, photo, nickname, given, family, tel, info),
|
|
}
|
|
log.Debugf("%#v", answer)
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
_ = gateway.ResumableSend(component, &answer)
|
|
}
|
|
|
|
func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) {
|
|
answer, err := stanza.NewIQ(stanza.Attrs{
|
|
Type: stanza.IQTypeResult,
|
|
From: iq.To,
|
|
To: iq.From,
|
|
Id: iq.Id,
|
|
Lang: "en",
|
|
})
|
|
if err != nil {
|
|
log.Errorf("Failed to create answer IQ: %v", err)
|
|
return
|
|
}
|
|
|
|
if dt == discoTypeInfo {
|
|
disco := answer.DiscoInfo()
|
|
toID, toOk := toToID(iq.To)
|
|
if !toOk {
|
|
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
|
|
}
|
|
|
|
var isMuc bool
|
|
bare, _, fromOk := splitFrom(iq.From)
|
|
if fromOk {
|
|
session, sessionOk := sessions[bare]
|
|
if sessionOk && session.Session.MUC {
|
|
if toOk {
|
|
chat, _, err := session.GetContactByID(toID, nil)
|
|
if err == nil && session.IsGroup(chat) {
|
|
isMuc = true
|
|
disco.AddIdentity(chat.Title, "conference", "text")
|
|
|
|
disco.AddFeatures(
|
|
"http://jabber.org/protocol/muc",
|
|
"muc_persistent",
|
|
"muc_hidden",
|
|
"muc_membersonly",
|
|
"muc_unmoderated",
|
|
"muc_nonanonymous",
|
|
"muc_unsecured",
|
|
)
|
|
fields := []*stanza.Field{
|
|
&stanza.Field{
|
|
Var: "FORM_TYPE",
|
|
Type: "hidden",
|
|
ValuesList: []string{"http://jabber.org/protocol/muc#roominfo"},
|
|
},
|
|
&stanza.Field{
|
|
Var: "muc#roominfo_description",
|
|
Label: "Description",
|
|
ValuesList: []string{session.GetChatDescription(chat)},
|
|
},
|
|
&stanza.Field{
|
|
Var: "muc#roominfo_occupants",
|
|
Label: "Number of occupants",
|
|
ValuesList: []string{strconv.FormatInt(int64(session.GetChatMemberCount(chat)), 10)},
|
|
},
|
|
}
|
|
|
|
disco.Form = stanza.NewForm(fields, "result")
|
|
}
|
|
} else {
|
|
disco.AddFeatures(stanza.NSDiscoItems)
|
|
disco.AddIdentity("Telegram group chats", "conference", "text")
|
|
}
|
|
}
|
|
}
|
|
if toOk && !isMuc {
|
|
disco.AddIdentity("", "account", "registered")
|
|
}
|
|
answer.Payload = disco
|
|
} else if dt == discoTypeItems {
|
|
disco := answer.DiscoItems()
|
|
|
|
_, ok := toToID(iq.To)
|
|
if !ok {
|
|
bare, _, ok := splitFrom(iq.From)
|
|
if ok {
|
|
// raw access, no need to create a new instance if not connected
|
|
session, ok := sessions[bare]
|
|
if ok && session.Session.MUC {
|
|
bareJid := gateway.Jid.Bare()
|
|
disco.AddItem(bareJid, "", "Telegram group chats")
|
|
for _, chat := range session.GetGroupChats() {
|
|
jid := strconv.FormatInt(chat.Id, 10) + "@" + bareJid
|
|
disco.AddItem(jid, "", chat.Title)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
answer.Payload = disco
|
|
}
|
|
|
|
log.Debugf("%#v", answer)
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
_ = 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 {
|
|
return 0, false
|
|
}
|
|
toID, err := strconv.ParseInt(toParts[0], 10, 64)
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"to": to,
|
|
}).Error(errors.Wrap(err, "Invalid to JID!"))
|
|
return 0, false
|
|
}
|
|
return toID, true
|
|
}
|
|
|
|
func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, info string) stanza.IQPayload {
|
|
if typ == TypeVCardTemp {
|
|
vcard := &extensions.IqVcardTemp{}
|
|
|
|
vcard.Fn.Text = fn
|
|
if photo != "" {
|
|
vcard.Photo.Type.Text = "image/jpeg"
|
|
vcard.Photo.Binval.Text = photo
|
|
}
|
|
vcard.Nickname.Text = nickname
|
|
vcard.N.Given.Text = given
|
|
vcard.N.Family.Text = family
|
|
vcard.Tel.Number.Text = tel
|
|
vcard.Desc.Text = info
|
|
|
|
return vcard
|
|
} else if typ == TypeVCard4 {
|
|
nodes := []stanza.Node{}
|
|
if fn != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "fn"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "text"},
|
|
Content: fn,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if photo != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "photo"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "uri"},
|
|
Content: "data:image/jpeg;base64," + photo,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if nickname != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "nickname"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "text"},
|
|
Content: nickname,
|
|
},
|
|
},
|
|
}, stanza.Node{
|
|
XMLName: xml.Name{Local: "impp"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "uri"},
|
|
Content: "https://t.me/" + nickname,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if family != "" || given != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "n"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "surname"},
|
|
Content: family,
|
|
},
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "given"},
|
|
Content: given,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if tel != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "tel"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "uri"},
|
|
Content: "tel:" + tel,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if info != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "note"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "text"},
|
|
Content: info,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
pubsub := &stanza.PubSubGeneric{
|
|
Items: &stanza.Items{
|
|
Node: NodeVCard4,
|
|
List: []stanza.Item{
|
|
stanza.Item{
|
|
Id: id,
|
|
Any: &stanza.Node{
|
|
XMLName: xml.Name{Local: "vcard"},
|
|
Attrs: []xml.Attr{
|
|
xml.Attr{
|
|
Name: xml.Name{Local: "xmlns"},
|
|
Value: "urn:ietf:params:xml:ns:vcard-4.0",
|
|
},
|
|
},
|
|
Nodes: nodes,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return pubsub
|
|
}
|
|
|
|
return nil
|
|
}
|