995 lines
23 KiB
Go
995 lines
23 KiB
Go
package xmpp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"github.com/pkg/errors"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"dev.narayana.im/narayana/telegabber/persistence"
|
|
"dev.narayana.im/narayana/telegabber/telegram"
|
|
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
|
|
"dev.narayana.im/narayana/telegabber/xmpp/gateway"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/soheilhy/args"
|
|
"gosrc.io/xmpp"
|
|
"gosrc.io/xmpp/stanza"
|
|
)
|
|
|
|
const (
|
|
TypeVCardTemp byte = iota
|
|
TypeVCard4
|
|
)
|
|
const NodeVCard4 string = "urn:xmpp:vcard4"
|
|
const NSCommand string = "http://jabber.org/protocol/commands"
|
|
|
|
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
|
|
}
|
|
}
|
|
discoInfo, ok := iq.Payload.(*stanza.DiscoInfo)
|
|
if ok {
|
|
go handleGetDiscoInfo(s, iq, discoInfo)
|
|
return
|
|
}
|
|
discoItems, ok := iq.Payload.(*stanza.DiscoItems)
|
|
if ok {
|
|
go handleGetDiscoItems(s, iq, discoItems)
|
|
return
|
|
}
|
|
_, ok = iq.Payload.(*extensions.QueryRegister)
|
|
if ok {
|
|
go handleGetQueryRegister(s, iq)
|
|
return
|
|
}
|
|
} else if iq.Type == "set" {
|
|
query, ok := iq.Payload.(*extensions.QueryRegister)
|
|
if ok {
|
|
go handleSetQueryRegister(s, iq, query)
|
|
return
|
|
}
|
|
command, ok := iq.Payload.(*stanza.Command)
|
|
if ok {
|
|
go handleSetQueryCommand(s, iq, command)
|
|
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 := gateway.SplitJID(msg.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
gatewayJid := gateway.Jid.Bare()
|
|
|
|
session, ok := sessions[bare]
|
|
if !ok {
|
|
if msg.To == gatewayJid {
|
|
gateway.SubscribeToTransport(component, msg.From)
|
|
} else {
|
|
log.Error("Message from stranger")
|
|
}
|
|
return
|
|
}
|
|
|
|
toID, ok := toToID(msg.To)
|
|
if ok {
|
|
var reply extensions.Reply
|
|
var fallback extensions.Fallback
|
|
var replace extensions.Replace
|
|
msg.Get(&reply)
|
|
msg.Get(&fallback)
|
|
msg.Get(&replace)
|
|
log.Debugf("reply: %#v", reply)
|
|
log.Debugf("fallback: %#v", fallback)
|
|
log.Debugf("replace: %#v", replace)
|
|
|
|
var replyId int64
|
|
var err error
|
|
text := msg.Body
|
|
if len(reply.Id) > 0 {
|
|
chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, reply.Id)
|
|
if err == nil {
|
|
if chatId != toID {
|
|
log.Warnf("Chat mismatch: %v ≠ %v", chatId, toID)
|
|
} else {
|
|
replyId = msgId
|
|
log.Debugf("replace tg: %#v %#v", chatId, msgId)
|
|
}
|
|
} else {
|
|
id := reply.Id
|
|
if id[0] == 'e' {
|
|
id = id[1:]
|
|
}
|
|
replyId, err = strconv.ParseInt(id, 10, 64)
|
|
if err != nil {
|
|
log.Warn(errors.Wrap(err, "Failed to parse message ID!"))
|
|
}
|
|
}
|
|
|
|
if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 {
|
|
body := fallback.Body[0]
|
|
var start, end int64
|
|
start, err = strconv.ParseInt(body.Start, 10, 64)
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"start": body.Start,
|
|
}).Warn(errors.Wrap(err, "Failed to parse fallback start!"))
|
|
}
|
|
end, err = strconv.ParseInt(body.End, 10, 64)
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"end": body.End,
|
|
}).Warn(errors.Wrap(err, "Failed to parse fallback end!"))
|
|
}
|
|
|
|
fullRunes := []rune(text)
|
|
cutRunes := make([]rune, 0, len(text)-int(end-start))
|
|
cutRunes = append(cutRunes, fullRunes[:start]...)
|
|
cutRunes = append(cutRunes, fullRunes[end:]...)
|
|
text = string(cutRunes)
|
|
}
|
|
}
|
|
var replaceId int64
|
|
if replace.Id != "" {
|
|
chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, replace.Id)
|
|
if err == nil {
|
|
if chatId != toID {
|
|
gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "<ERROR: Chat mismatch>", component)
|
|
return
|
|
}
|
|
replaceId = msgId
|
|
log.Debugf("replace tg: %#v %#v", chatId, msgId)
|
|
} else {
|
|
gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "<ERROR: Could not find matching message to edit>", component)
|
|
return
|
|
}
|
|
}
|
|
|
|
session.SendMessageLock.Lock()
|
|
defer session.SendMessageLock.Unlock()
|
|
tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId)
|
|
if tgMessageId != 0 {
|
|
if replaceId != 0 {
|
|
// not needed (is it persistent among clients though?)
|
|
/* err = gateway.IdsDB.ReplaceIdPair(session.Session.Login, bare, replace.Id, msg.Id, tgMessageId)
|
|
if err != nil {
|
|
log.Errorf("Failed to replace id %v with %v %v", replace.Id, msg.Id, tgMessageId)
|
|
} */
|
|
session.AddToEditOutbox(replace.Id, resource)
|
|
} else {
|
|
err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id)
|
|
if err == nil {
|
|
session.AddToOutbox(msg.Id, resource)
|
|
session.UpdateLastChatMessageId(toID, msg.Id)
|
|
} else {
|
|
log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id)
|
|
}
|
|
}
|
|
} else {
|
|
/*
|
|
// if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway
|
|
if replaceId != 0 {
|
|
err = gateway.IdsDB.ReplaceXmppId(session.Session.Login, bare, replace.Id, msg.Id)
|
|
if err != nil {
|
|
log.Errorf("Failed to replace id %v with %v", replace.Id, msg.Id)
|
|
}
|
|
} */
|
|
}
|
|
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.SendServiceMessage(msg.From, response, component)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
log.Warn("Unknown purpose of the message, skipping")
|
|
}
|
|
|
|
if msg.Body == "" {
|
|
var privilege1 extensions.ComponentPrivilege1
|
|
if ok := msg.Get(&privilege1); ok {
|
|
log.Debugf("privilege1: %#v", privilege1)
|
|
}
|
|
|
|
for _, perm := range privilege1.Perms {
|
|
if perm.Access == "message" && perm.Type == "outgoing" {
|
|
gateway.MessageOutgoingPermissionVersion = 1
|
|
}
|
|
}
|
|
|
|
var privilege2 extensions.ComponentPrivilege2
|
|
if ok := msg.Get(&privilege2); ok {
|
|
log.Debugf("privilege2: %#v", privilege2)
|
|
}
|
|
|
|
for _, perm := range privilege2.Perms {
|
|
if perm.Access == "message" && perm.Type == "outgoing" {
|
|
gateway.MessageOutgoingPermissionVersion = 2
|
|
}
|
|
}
|
|
|
|
var displayed stanza.MarkDisplayed
|
|
msg.Get(&displayed)
|
|
if displayed.ID != "" {
|
|
log.Debugf("displayed: %#v", displayed)
|
|
|
|
bare, _, ok := gateway.SplitJID(msg.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
session, ok := sessions[bare]
|
|
if !ok {
|
|
return
|
|
}
|
|
toID, ok := toToID(msg.To)
|
|
if !ok {
|
|
return
|
|
}
|
|
msgId, err := strconv.ParseInt(displayed.ID, 10, 64)
|
|
if err == nil {
|
|
session.MarkAsRead(toID, msgId)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if msg.Type == "error" {
|
|
log.Errorf("MESSAGE ERROR: %#v", p)
|
|
|
|
if msg.XMLName.Space == "jabber:component:accept" && msg.Error.Code == 401 {
|
|
suffix := "@" + msg.From
|
|
for bare := range sessions {
|
|
if strings.HasSuffix(bare, suffix) {
|
|
gateway.SendServiceMessage(bare, "Your server \""+msg.From+"\" does not allow to send carbons", component)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 := gateway.SplitJID(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 := gateway.SplitJID(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() {
|
|
show, description, typ := status.Destruct()
|
|
newArgs := []args.V{
|
|
gateway.SPImmed(false),
|
|
}
|
|
if typ != "" {
|
|
newArgs = append(newArgs, gateway.SPType(typ))
|
|
}
|
|
go session.ProcessStatusUpdate(
|
|
status.ID,
|
|
description,
|
|
show,
|
|
newArgs...,
|
|
)
|
|
}
|
|
session.UpdateChatNicknames()
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
info, err := session.GetVcardInfo(toID)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
|
|
answer := stanza.IQ{
|
|
Attrs: stanza.Attrs{
|
|
From: iq.To,
|
|
To: iq.From,
|
|
Id: iq.Id,
|
|
Type: "result",
|
|
},
|
|
Payload: makeVCardPayload(typ, iq.To, info, session),
|
|
}
|
|
log.Debugf("%#v", answer)
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
_ = gateway.ResumableSend(component, &answer)
|
|
}
|
|
|
|
func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
|
|
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
|
|
}
|
|
|
|
disco := answer.DiscoInfo()
|
|
_, ok := toToID(iq.To)
|
|
if ok {
|
|
disco.AddIdentity("", "account", "registered")
|
|
disco.AddFeatures(stanza.NSMsgChatMarkers)
|
|
disco.AddFeatures(stanza.NSMsgReceipts)
|
|
} else {
|
|
if di.Node == "" {
|
|
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
|
|
disco.AddFeatures("jabber:iq:register")
|
|
disco.AddFeatures(NSCommand)
|
|
} else {
|
|
for name, command := range telegram.GetCommands(telegram.CommandTypeTransport) {
|
|
if di.Node == name {
|
|
answer.Payload = di
|
|
di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node")
|
|
di.AddFeatures(NSCommand, "jabber:x:data")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) {
|
|
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
|
|
}
|
|
|
|
log.Debugf("discoItems: %#v", di)
|
|
|
|
_, ok := toToID(iq.To)
|
|
if !ok {
|
|
commands := telegram.GetCommands(telegram.CommandTypeTransport)
|
|
if di.Node == NSCommand {
|
|
answer.Payload = di
|
|
for name, command := range commands {
|
|
di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command))
|
|
}
|
|
} else {
|
|
answer.Payload = answer.DiscoItems()
|
|
}
|
|
}
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
_ = gateway.ResumableSend(component, answer)
|
|
}
|
|
|
|
func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) {
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var login string
|
|
bare, _, ok := gateway.SplitJID(iq.From)
|
|
if ok {
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
login = session.Session.Login
|
|
}
|
|
}
|
|
|
|
var query stanza.IQPayload
|
|
if login == "" {
|
|
query = extensions.QueryRegister{
|
|
Instructions: fmt.Sprintf("Authorization in Telegram is a multi-step process, so please accept %v to your contacts and follow further instructions (provide the authentication code there, etc.).\nFor now, please provide your login.", iq.To),
|
|
}
|
|
} else {
|
|
query = extensions.QueryRegister{
|
|
Instructions: "Already logged in",
|
|
Username: login,
|
|
Registered: &extensions.QueryRegisterRegistered{},
|
|
}
|
|
}
|
|
answer.Payload = query
|
|
|
|
log.Debugf("%#v", query)
|
|
|
|
_ = gateway.ResumableSend(component, answer)
|
|
|
|
if login == "" {
|
|
gateway.SubscribeToTransport(component, iq.From)
|
|
}
|
|
}
|
|
|
|
func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) {
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
if query.Remove != nil {
|
|
iqAnswerSetError(answer, query, 405)
|
|
return
|
|
}
|
|
|
|
var login string
|
|
var session *telegram.Client
|
|
bare, resource, ok := gateway.SplitJID(iq.From)
|
|
if ok {
|
|
session, ok = sessions[bare]
|
|
if ok {
|
|
login = session.Session.Login
|
|
}
|
|
}
|
|
|
|
if login == "" {
|
|
if !ok {
|
|
session, ok = getTelegramInstance(bare, &persistence.Session{}, component)
|
|
if !ok {
|
|
iqAnswerSetError(answer, query, 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
err := session.TryLogin(resource, query.Username)
|
|
if err != nil {
|
|
if err.Error() == telegram.TelegramAuthDone {
|
|
iqAnswerSetError(answer, query, 406)
|
|
} else {
|
|
iqAnswerSetError(answer, query, 500)
|
|
}
|
|
return
|
|
}
|
|
|
|
err = session.SetPhoneNumber(query.Username)
|
|
if err != nil {
|
|
iqAnswerSetError(answer, query, 500)
|
|
return
|
|
}
|
|
|
|
// everything okay, the response should be empty with no payload/error at this point
|
|
gateway.SubscribeToTransport(component, iq.From)
|
|
} else {
|
|
iqAnswerSetError(answer, query, 406)
|
|
}
|
|
}
|
|
|
|
func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command) {
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
log.Debugf("command: %#v", command)
|
|
|
|
bare, resource, ok := gateway.SplitJID(iq.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
_, toOk := toToID(iq.To)
|
|
|
|
var cmdString string
|
|
if !toOk {
|
|
form, formOk := command.CommandElement.(*stanza.Form)
|
|
if formOk {
|
|
// just for the case the client messed the order somehow
|
|
sort.Slice(form.Fields, func(i int, j int) bool {
|
|
iField := form.Fields[i]
|
|
jField := form.Fields[j]
|
|
if iField != nil && jField != nil {
|
|
ii, iErr := strconv.ParseInt(iField.Var, 10, 64)
|
|
ji, jErr := strconv.ParseInt(jField.Var, 10, 64)
|
|
return iErr == nil && jErr == nil && ii < ji
|
|
}
|
|
return false
|
|
})
|
|
|
|
var cmd strings.Builder
|
|
cmd.WriteString("/")
|
|
cmd.WriteString(command.Node)
|
|
for _, field := range form.Fields {
|
|
cmd.WriteString(" ")
|
|
if len(field.ValuesList) > 0 {
|
|
cmd.WriteString(field.ValuesList[0])
|
|
}
|
|
}
|
|
|
|
cmdString = cmd.String()
|
|
} else {
|
|
if command.Action == "" || command.Action == stanza.CommandActionExecute {
|
|
cmd, ok := telegram.GetCommand(telegram.CommandTypeTransport, command.Node)
|
|
if ok && cmd.RequiredArgs > 0 {
|
|
var fields []*stanza.Field
|
|
for i, arg := range cmd.Arguments {
|
|
fields = append(fields, &stanza.Field{
|
|
Var: strconv.FormatInt(int64(i), 10),
|
|
Label: arg,
|
|
})
|
|
}
|
|
answer.Payload = &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
Status: stanza.CommandStatusExecuting,
|
|
CommandElement: &stanza.Form{
|
|
Type: stanza.FormTypeForm,
|
|
Title: command.Node,
|
|
Instructions: []string{cmd.Description},
|
|
Fields: fields,
|
|
},
|
|
}
|
|
} else {
|
|
cmdString = "/" + command.Node
|
|
}
|
|
} else if command.Action == stanza.CommandActionCancel {
|
|
answer.Payload = &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
Status: stanza.CommandStatusCancelled,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if cmdString != "" {
|
|
session, ok := sessions[bare]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
response := session.ProcessTransportCommand(cmdString, resource)
|
|
|
|
answer.Payload = &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
Status: stanza.CommandStatusCompleted,
|
|
CommandElement: &stanza.Note{
|
|
Text: response,
|
|
Type: stanza.CommandNoteTypeInfo,
|
|
},
|
|
}
|
|
|
|
log.Debugf("command response: %#v", answer.Payload)
|
|
}
|
|
}
|
|
|
|
func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) {
|
|
answer.Type = stanza.IQTypeError
|
|
answer.Payload = *payload
|
|
switch code {
|
|
case 400:
|
|
answer.Error = &stanza.Err{
|
|
Code: code,
|
|
Type: stanza.ErrorTypeModify,
|
|
Reason: "bad-request",
|
|
}
|
|
case 405:
|
|
answer.Error = &stanza.Err{
|
|
Code: code,
|
|
Type: stanza.ErrorTypeCancel,
|
|
Reason: "not-allowed",
|
|
Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport",
|
|
}
|
|
case 406:
|
|
answer.Error = &stanza.Err{
|
|
Code: code,
|
|
Type: stanza.ErrorTypeModify,
|
|
Reason: "not-acceptable",
|
|
Text: "Phone number already provided, chat with the transport for further instruction",
|
|
}
|
|
case 500:
|
|
answer.Error = &stanza.Err{
|
|
Code: code,
|
|
Type: stanza.ErrorTypeWait,
|
|
Reason: "internal-server-error",
|
|
}
|
|
default:
|
|
log.Error("Unknown error code, falling back with empty reason")
|
|
answer.Error = &stanza.Err{
|
|
Code: code,
|
|
Type: stanza.ErrorTypeCancel,
|
|
Reason: "undefined-condition",
|
|
}
|
|
}
|
|
}
|
|
|
|
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 string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload {
|
|
var base64Photo string
|
|
if info.Photo != nil {
|
|
file, path, err := session.ForceOpenFile(info.Photo, 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 {
|
|
base64Photo = 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())
|
|
}
|
|
}
|
|
|
|
if typ == TypeVCardTemp {
|
|
vcard := &extensions.IqVcardTemp{}
|
|
|
|
vcard.Fn.Text = info.Fn
|
|
if base64Photo != "" {
|
|
vcard.Photo.Type.Text = "image/jpeg"
|
|
vcard.Photo.Binval.Text = base64Photo
|
|
}
|
|
vcard.Nickname.Text = strings.Join(info.Nicknames, ",")
|
|
vcard.N.Given.Text = info.Given
|
|
vcard.N.Family.Text = info.Family
|
|
vcard.Tel.Number.Text = info.Tel
|
|
vcard.Desc.Text = info.Info
|
|
|
|
return vcard
|
|
} else if typ == TypeVCard4 {
|
|
nodes := []stanza.Node{}
|
|
if info.Fn != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "fn"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "text"},
|
|
Content: info.Fn,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if base64Photo != "" {
|
|
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," + base64Photo,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
for _, nickname := range info.Nicknames {
|
|
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 info.Family != "" || info.Given != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "n"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "surname"},
|
|
Content: info.Family,
|
|
},
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "given"},
|
|
Content: info.Given,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if info.Tel != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "tel"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "uri"},
|
|
Content: "tel:" + info.Tel,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if info.Info != "" {
|
|
nodes = append(nodes, stanza.Node{
|
|
XMLName: xml.Name{Local: "note"},
|
|
Nodes: []stanza.Node{
|
|
stanza.Node{
|
|
XMLName: xml.Name{Local: "text"},
|
|
Content: info.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
|
|
}
|