mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
2111 lines
52 KiB
Go
2111 lines
52 KiB
Go
package xmpp
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"github.com/pkg/errors"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
|
|
"github.com/google/uuid"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/soheilhy/args"
|
|
"gosrc.io/xmpp"
|
|
"gosrc.io/xmpp/stanza"
|
|
)
|
|
|
|
const (
|
|
TypeVCardTemp byte = iota
|
|
TypeVCard4
|
|
)
|
|
|
|
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 == stanza.IQTypeGet {
|
|
_, ok := iq.Payload.(*extensions.IqVcardTemp)
|
|
if ok {
|
|
go handleGetVcardIq(s, iq, TypeVCardTemp)
|
|
return
|
|
}
|
|
pubsub, ok := iq.Payload.(*stanza.PubSubGeneric)
|
|
if ok && pubsub.Items != nil {
|
|
if pubsub.Items.Node == gateway.NodeVCard4 {
|
|
go handleGetVcardIq(s, iq, TypeVCard4)
|
|
return
|
|
}
|
|
if pubsub.Items.Node == gateway.NodeAvatarData {
|
|
go handleGetAvatarDataIq(s, iq, pubsub)
|
|
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
|
|
}
|
|
queryMucAdmin, ok := iq.Payload.(*extensions.QueryMucAdmin)
|
|
if ok {
|
|
go handleGetQueryMucAdmin(s, iq, queryMucAdmin)
|
|
return
|
|
}
|
|
} else if iq.Type == stanza.IQTypeSet {
|
|
queryRegister, ok := iq.Payload.(*extensions.QueryRegister)
|
|
if ok {
|
|
go handleSetQueryRegister(s, iq, queryRegister)
|
|
return
|
|
}
|
|
command, ok := iq.Payload.(*stanza.Command)
|
|
if ok {
|
|
go handleSetQueryCommand(s, iq, command)
|
|
return
|
|
}
|
|
queryMucAdmin, ok := iq.Payload.(*extensions.QueryMucAdmin)
|
|
if ok {
|
|
go handleSetQueryMucAdmin(s, iq, queryMucAdmin)
|
|
return
|
|
}
|
|
} else if iq.Type == stanza.IQTypeResult {
|
|
discoInfo, ok := iq.Payload.(*stanza.DiscoInfo)
|
|
if ok {
|
|
go handleClientFeatures(s, iq, discoInfo)
|
|
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, toIsGroup := toToID(msg.To)
|
|
if ok {
|
|
toJid, err := stanza.NewJid(msg.To)
|
|
if err != nil {
|
|
log.Error("Invalid to JID!")
|
|
return
|
|
}
|
|
|
|
isGroupchat := msg.Type == "groupchat"
|
|
|
|
if session.Session.MUC {
|
|
chat, _, err := session.GetContactByID(toID, nil)
|
|
if err == nil && session.IsGroup(chat) {
|
|
if !toIsGroup {
|
|
gateway.SendErrorMessage(msg.From, toJid.Node, "KHVATIT SYUDA ZVONITb", 403, false, component)
|
|
return
|
|
}
|
|
|
|
if toJid.Resource != "" {
|
|
if isGroupchat {
|
|
gateway.SendErrorMessageWithBody(msg.From, msg.To, msg.Body, "", msg.Id, 400, true, component)
|
|
} else {
|
|
gateway.SendErrorMessage(msg.From, msg.To, "PMing room occupants is not supported, use the real JID", 406, true, component)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
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' {
|
|
idParts := strings.Split(id[1:], ":")
|
|
if len(idParts) >= 1 {
|
|
id = idParts[0]
|
|
}
|
|
}
|
|
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 {
|
|
if isGroupchat {
|
|
gateway.SendErrorMessage(msg.From, gateway.MUCJID(toID), text, 400, isGroupchat, component)
|
|
} else {
|
|
gateway.SendTextMessage(msg.From, gateway.CHATNODE(toID), "<ERROR: Chat mismatch>", component, isGroupchat)
|
|
}
|
|
return
|
|
}
|
|
replaceId = msgId
|
|
log.Debugf("replace tg: %#v %#v", chatId, msgId)
|
|
} else {
|
|
if isGroupchat {
|
|
gateway.SendErrorMessage(msg.From, gateway.MUCJID(toID), text, 400, isGroupchat, component)
|
|
} else {
|
|
gateway.SendTextMessage(msg.From, gateway.CHATNODE(toID), "<ERROR: Could not find matching message to edit>", component, isGroupchat)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
session.SendMessageLock.Lock()
|
|
defer session.SendMessageLock.Unlock()
|
|
tgMessage := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId, isGroupchat, false)
|
|
if tgMessage != nil {
|
|
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, tgMessage.Id, 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, tgMessage.Id, msg.Id)
|
|
}
|
|
}
|
|
|
|
// pong groupchat messages back
|
|
if isGroupchat && toJid.Resource == "" && tgMessage.SenderId != nil {
|
|
session.SendMessageToGateway(
|
|
toID,
|
|
tgMessage,
|
|
msg.Id,
|
|
false,
|
|
msg.To + "/" + session.GetMUCNickname(session.GetSenderId(tgMessage.SenderId)),
|
|
[]string{msg.From},
|
|
)
|
|
}
|
|
} 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.Thread == "" && msg.Subject != "" && msg.Type == "groupchat" {
|
|
log.Debugf("MUC subject change: %#v", msg)
|
|
|
|
bare, _, ok := gateway.SplitJID(msg.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
session, ok := sessions[bare]
|
|
if !ok {
|
|
return
|
|
}
|
|
toID, ok, isGroup := toToID(msg.To)
|
|
if !ok || !isGroup {
|
|
return
|
|
}
|
|
_, resource, ok := gateway.SplitJID(msg.To)
|
|
if ok && resource != "" {
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
pinOk := session.NewPinnedMessage(toID, msg.Subject, msg.From)
|
|
if !pinOk {
|
|
gateway.SendErrorMessage(msg.From, gateway.MUCJID(toID), "", 406, true, component)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
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)
|
|
return
|
|
}
|
|
var mucExt stanza.MucPresence
|
|
prs.Get(&mucExt)
|
|
if mucExt.XMLName.Space != "" {
|
|
handleMUCPresence(s, prs, mucExt)
|
|
return
|
|
}
|
|
tryHandleMUCPresence(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...,
|
|
)
|
|
}
|
|
probeClientFeatures(p.From, component)
|
|
session.UpdateChatNicknames()
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func handleMUCPresence(s xmpp.Sender, p stanza.Presence, mucExt stanza.MucPresence) {
|
|
log.WithFields(log.Fields{
|
|
"type": p.Type,
|
|
"from": p.From,
|
|
"to": p.To,
|
|
}).Warn("MUC presence")
|
|
log.Debugf("%#v", p)
|
|
|
|
if p.Type == "" {
|
|
toBare, nickname, ok := gateway.SplitJID(p.To)
|
|
if ok {
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
// separate declaration is crucial for passing as pointer to defer
|
|
var reply *stanza.Presence
|
|
reply = &stanza.Presence{Attrs: stanza.Attrs{
|
|
From: toBare,
|
|
To: p.From,
|
|
Id: p.Id,
|
|
}}
|
|
defer gateway.ResumableSend(component, reply)
|
|
|
|
if nickname == "" {
|
|
presenceReplySetError(reply, 400)
|
|
return
|
|
}
|
|
|
|
chatId, ok, toIsGroup := toToID(toBare)
|
|
if !ok || !toIsGroup {
|
|
presenceReplySetError(reply, 404)
|
|
return
|
|
}
|
|
|
|
fromBare, fromResource, ok := gateway.SplitJID(p.From)
|
|
if !ok {
|
|
presenceReplySetError(reply, 400)
|
|
return
|
|
}
|
|
|
|
session, ok := sessions[fromBare]
|
|
if !ok || !session.Session.MUC {
|
|
presenceReplySetError(reply, 407)
|
|
return
|
|
}
|
|
|
|
chat, _, err := session.GetContactByID(chatId, nil)
|
|
if err != nil || !session.IsGroup(chat) {
|
|
presenceReplySetError(reply, 404)
|
|
return
|
|
}
|
|
|
|
log.Debugf("%#v", mucExt)
|
|
maxStanzas, maxStanzasOk := mucExt.History.MaxStanzas.Get()
|
|
maxChars, maxCharsOk := mucExt.History.MaxChars.Get()
|
|
seconds, secondsOk := mucExt.History.Seconds.Get()
|
|
|
|
var limit *telegram.MessageLimit
|
|
if maxStanzasOk {
|
|
limit = telegram.NewMessageLimitMessages(int32(maxStanzas))
|
|
} else if maxCharsOk {
|
|
limit = telegram.NewMessageLimitChars(maxChars)
|
|
} else if secondsOk {
|
|
limit = telegram.NewMessageLimitSince(time.Now().Add(time.Duration(seconds) * -time.Second).Unix())
|
|
} else if !mucExt.History.Since.IsZero() {
|
|
limit = telegram.NewMessageLimitSince(mucExt.History.Since.Unix())
|
|
} else {
|
|
limit = telegram.NewMessageLimitMessages(20)
|
|
}
|
|
session.JoinMUC(chatId, fromResource, limit)
|
|
}
|
|
}
|
|
}
|
|
|
|
func tryHandleMUCPresence(s xmpp.Sender, p stanza.Presence) {
|
|
toBare, nickname, ok := gateway.SplitJID(p.To)
|
|
if !ok || nickname == "" {
|
|
return
|
|
}
|
|
|
|
log.WithFields(log.Fields{
|
|
"type": p.Type,
|
|
"from": p.From,
|
|
"to": p.To,
|
|
}).Warn("Nickname change presence?")
|
|
log.Debugf("%#v", p)
|
|
|
|
fromBare, fromResource, ok := gateway.SplitJID(p.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
session, ok := sessions[fromBare]
|
|
if !ok || !session.Session.MUC {
|
|
return
|
|
}
|
|
|
|
chatId, ok, toIsGroup := toToID(toBare)
|
|
if !ok || !toIsGroup {
|
|
return
|
|
}
|
|
|
|
chat, _, err := session.GetContactByID(chatId, nil)
|
|
if err != nil || !session.IsGroup(chat) {
|
|
return
|
|
}
|
|
|
|
if !session.MUCHasResource(chatId, fromResource) {
|
|
return
|
|
}
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
switch p.Type {
|
|
case "":
|
|
handleMUCNicknameChange(component, p, session, chatId, toBare)
|
|
case stanza.PresenceTypeUnavailable:
|
|
handleMUCUnavailable(component, p, session, chatId, fromResource)
|
|
}
|
|
}
|
|
|
|
func handleMUCNicknameChange(component *xmpp.Component, p stanza.Presence, session *telegram.Client, chatId int64, toBare string) {
|
|
log.Warn("🗿 Yes")
|
|
|
|
from := toBare
|
|
nickname, ok := session.GetMyMUCNickname(chatId)
|
|
if ok {
|
|
from = from+"/"+nickname
|
|
}
|
|
reply := &stanza.Presence{
|
|
Attrs: stanza.Attrs{
|
|
From: from,
|
|
To: p.From,
|
|
Id: p.Id,
|
|
Type: stanza.PresenceTypeError,
|
|
},
|
|
Error: stanza.Err{
|
|
Code: 406,
|
|
Type: stanza.ErrorTypeModify,
|
|
Reason: "not-acceptable",
|
|
Text: "Telegram does not support changing nicknames per-chat. Issue a /setname command to the transport if you wish to change the global name",
|
|
},
|
|
}
|
|
gateway.ResumableSend(component, reply)
|
|
}
|
|
|
|
func handleMUCUnavailable(component *xmpp.Component, p stanza.Presence, session *telegram.Client, chatId int64, resource string) {
|
|
log.Warn("No, it's a MUC exit")
|
|
|
|
session.LeaveMUC(chatId, resource)
|
|
|
|
reply := &stanza.Presence{
|
|
Attrs: stanza.Attrs{
|
|
From: p.To,
|
|
To: p.From,
|
|
Id: p.Id,
|
|
Type: stanza.PresenceTypeUnavailable,
|
|
},
|
|
Extensions: []stanza.PresExtension{
|
|
extensions.PresenceXMucUserExtension{
|
|
Item: extensions.PresenceXMucUserItem{
|
|
Affiliation: "member",
|
|
Jid: p.From,
|
|
Role: "none",
|
|
},
|
|
Statuses: []extensions.PresenceXMucUserStatus{
|
|
extensions.PresenceXMucUserStatus{Code: 110},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
gateway.ResumableSend(component, reply)
|
|
}
|
|
|
|
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 handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGeneric) {
|
|
fromJid, err := stanza.NewJid(iq.From)
|
|
if err != nil {
|
|
log.Errorf("Invalid from JID %v", iq.From)
|
|
return
|
|
}
|
|
|
|
chatId, ok, _ := toToID(iq.To)
|
|
if !ok {
|
|
log.Errorf("Invalid chat id in To JID %v", iq.To)
|
|
return
|
|
}
|
|
|
|
session, ok := sessions[fromJid.Bare()]
|
|
if !ok {
|
|
log.Errorf("IQ from stranger %v", iq.From)
|
|
return
|
|
}
|
|
|
|
var id string
|
|
if len(pubsub.Items.List) > 0 {
|
|
id = pubsub.Items.List[0].Id
|
|
}
|
|
log.Infof("Avatar id %v for chat %v", id, iq.To)
|
|
|
|
pubsubAnswer := stanza.PubSubGeneric{
|
|
Items: &stanza.Items{
|
|
Node: gateway.NodeAvatarData,
|
|
},
|
|
}
|
|
|
|
answer := stanza.IQ{
|
|
Attrs: stanza.Attrs{
|
|
From: iq.To,
|
|
To: iq.From,
|
|
Id: iq.Id,
|
|
Type: "result",
|
|
},
|
|
Payload: &pubsubAnswer,
|
|
}
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
defer gateway.ResumableSend(component, &answer)
|
|
|
|
hashedAvatar, ok := session.AvatarHashes[chatId]
|
|
if !ok {
|
|
log.Info("Could not find avatar in cache, fetching immediately")
|
|
|
|
chat, _, err := session.GetContactByID(chatId, nil)
|
|
if err != nil || chat == nil || chat.Photo == nil {
|
|
return
|
|
}
|
|
|
|
file := chat.Photo.Small
|
|
|
|
sha1 := session.GetPhotoSha1(file, chatId)
|
|
hashedAvatar = &telegram.HashedAvatar{
|
|
Hash: sha1,
|
|
File: file.Id,
|
|
}
|
|
|
|
session.AvatarHashesLock.Lock()
|
|
session.AvatarHashes[chatId] = hashedAvatar
|
|
session.AvatarHashesLock.Unlock()
|
|
}
|
|
|
|
if id != "" && hashedAvatar.Hash != id {
|
|
log.Infof("Cache contains %v hash for chat %v, but %v was requested; aborting", hashedAvatar.Hash, iq.To, id)
|
|
return
|
|
}
|
|
if hashedAvatar.File == 0 {
|
|
log.Infof("Avatar for chat %v is explicitly missing", iq.To)
|
|
return
|
|
}
|
|
|
|
file, err := session.GetFile(hashedAvatar.File)
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"chatId": chatId,
|
|
}).Error(errors.Wrap(err, "Cannot get avatar file"))
|
|
return
|
|
}
|
|
|
|
dataString := session.GetPhotoBase64(file)
|
|
if dataString == "" {
|
|
log.Errorf("Error reading avatar file for chat %v", iq.To)
|
|
return
|
|
}
|
|
|
|
pubsubAnswer.Items.List = append(pubsubAnswer.Items.List, stanza.Item{
|
|
Id: hashedAvatar.Hash,
|
|
Any: &stanza.Node{
|
|
XMLName: xml.Name{Local: "data", Space: gateway.NodeAvatarData},
|
|
Content: dataString,
|
|
},
|
|
})
|
|
|
|
log.WithFields(log.Fields{
|
|
"length": len(dataString),
|
|
}).Debugf("%#v", answer)
|
|
}
|
|
|
|
func getTelegramChatType(from string, to string) (telegram.ChatType, error) {
|
|
toId, ok, _ := toToID(to)
|
|
if ok {
|
|
bare, _, ok := gateway.SplitJID(from)
|
|
if ok {
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
chatType, _, chatTypeErr := session.GetChatType(toId)
|
|
return chatType, chatTypeErr
|
|
}
|
|
}
|
|
}
|
|
|
|
return telegram.ChatTypeUnknown, errors.New("Unknown chat type")
|
|
}
|
|
|
|
func iqResultStub(s xmpp.Sender, iq *stanza.IQ) (*xmpp.Component, *stanza.IQ, bool) {
|
|
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 nil, nil, false
|
|
}
|
|
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return nil, nil, false
|
|
}
|
|
|
|
return component, answer, true
|
|
}
|
|
|
|
func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
|
|
component, answer, ok := iqResultStub(s, iq)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
disco := answer.DiscoInfo()
|
|
toID, toOk, toIsGroup := toToID(iq.To)
|
|
|
|
if di.Node == "" {
|
|
var isMuc bool
|
|
bare, _, fromOk := gateway.SplitJID(iq.From)
|
|
if fromOk {
|
|
session, sessionOk := sessions[bare]
|
|
if sessionOk && session.Session.MUC {
|
|
if toOk && toIsGroup {
|
|
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",
|
|
"http://jabber.org/protocol/muc#stable_id",
|
|
"jabber:iq:register",
|
|
"urn:xmpp:sid:0",
|
|
)
|
|
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 if !toOk {
|
|
disco.AddFeatures(
|
|
stanza.NSDiscoItems,
|
|
"http://jabber.org/protocol/muc#stable_id",
|
|
)
|
|
disco.AddIdentity("Telegram group chats", "conference", "text")
|
|
}
|
|
}
|
|
}
|
|
|
|
if toOk {
|
|
if !isMuc {
|
|
disco.AddIdentity("", "account", "registered")
|
|
}
|
|
disco.AddFeatures(stanza.NSMsgChatMarkers)
|
|
disco.AddFeatures(stanza.NSMsgReceipts)
|
|
} else {
|
|
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
|
|
disco.AddFeatures("jabber:iq:register")
|
|
}
|
|
disco.AddFeatures(gateway.NSCommand)
|
|
} else if di.Node == "x-roomuser-item" {
|
|
bare, _, fromOk := gateway.SplitJID(iq.From)
|
|
if fromOk {
|
|
session, sessionOk := sessions[bare]
|
|
if sessionOk && session.Session.MUC {
|
|
if toOk && toIsGroup {
|
|
chat, _, err := session.GetContactByID(toID, nil)
|
|
if err == nil && session.IsGroup(chat) {
|
|
disco.SetNode(di.Node)
|
|
disco.AddIdentity(session.GetMUCNickname(0), "conference", "text")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To)
|
|
|
|
var cmdType telegram.CommandType
|
|
if toOk {
|
|
cmdType = telegram.CommandTypeChat
|
|
} else {
|
|
cmdType = telegram.CommandTypeTransport
|
|
}
|
|
|
|
for name, command := range telegram.GetCommands(cmdType) {
|
|
if di.Node == name {
|
|
if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) {
|
|
break
|
|
}
|
|
disco.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node")
|
|
disco.AddFeatures(gateway.NSCommand, "jabber:x:data")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
answer.Payload = disco
|
|
}
|
|
|
|
func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) {
|
|
component, answer, ok := iqResultStub(s, iq)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
log.Debugf("discoItems: %#v", di)
|
|
|
|
toID, toOk, _ := toToID(iq.To)
|
|
|
|
disco := answer.DiscoItems()
|
|
|
|
if di.Node == gateway.NSCommand {
|
|
chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To)
|
|
|
|
var cmdType telegram.CommandType
|
|
if toOk {
|
|
cmdType = telegram.CommandTypeChat
|
|
} else {
|
|
cmdType = telegram.CommandTypeTransport
|
|
}
|
|
|
|
var isOnline bool
|
|
bare, _, ok := gateway.SplitJID(iq.From)
|
|
if ok {
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
isOnline = session.Online()
|
|
|
|
if toOk {
|
|
isBot, err := session.IsBot(toID)
|
|
if err == nil && isBot {
|
|
disco.AddItem(iq.To, "botmenu", "Bot Menu")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !(toOk || isOnline) {
|
|
disco.AddItem(iq.To, "loginwizard", "Login Wizard")
|
|
}
|
|
|
|
commands := telegram.GetCommands(cmdType)
|
|
for _, name := range telegram.SortedCommandKeys(commands) {
|
|
command := commands[name]
|
|
if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) {
|
|
continue
|
|
}
|
|
if !isOnline && command.LoginOnly {
|
|
continue
|
|
}
|
|
disco.AddItem(iq.To, name, telegram.CommandToHelpString(name, command))
|
|
}
|
|
} else if di.Node == "" {
|
|
if !toOk {
|
|
bare, _, fromOk := gateway.SplitJID(iq.From)
|
|
if fromOk {
|
|
// raw access, no need to create a new instance if not connected
|
|
session, sessionOk := sessions[bare]
|
|
if sessionOk && session.Session.MUC {
|
|
disco.AddItem(gateway.Jid.Bare(), "", "Telegram group chats")
|
|
for _, chat := range session.GetGroupChats() {
|
|
jid := gateway.MUCJID(chat.Id)
|
|
disco.AddItem(jid, "", chat.Title)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
answer.Payload = disco
|
|
|
|
log.Debugf("%#v", answer)
|
|
}
|
|
|
|
func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) {
|
|
component, answer, ok := iqResultStub(s, iq)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
_, toOk, toIsGroup := toToID(iq.To)
|
|
|
|
bare, _, ok := gateway.SplitJID(iq.From)
|
|
var session *telegram.Client
|
|
var sessionOk bool
|
|
if ok {
|
|
session, sessionOk = sessions[bare]
|
|
}
|
|
|
|
if toOk {
|
|
if toIsGroup {
|
|
nickname := "me"
|
|
if sessionOk {
|
|
nickname = session.GetMUCNickname(0)
|
|
}
|
|
answer.Payload = extensions.QueryRegister{
|
|
Instructions: "MUC username is static",
|
|
Username: nickname,
|
|
Registered: &extensions.QueryRegisterRegistered{},
|
|
}
|
|
} else {
|
|
query := extensions.QueryRegister{}
|
|
iqAnswerRegisterSetError(answer, &query, 404)
|
|
return
|
|
}
|
|
} else {
|
|
var login string
|
|
if sessionOk {
|
|
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)
|
|
|
|
if login == "" {
|
|
gateway.SubscribeToTransport(component, iq.From)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleGetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucAdmin) {
|
|
component, answer, ok := iqResultStub(s, iq)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
bare, _, fromOk := gateway.SplitJID(iq.From)
|
|
if !fromOk {
|
|
iqAnswerSetError(answer, 400)
|
|
return
|
|
}
|
|
|
|
session, sessionOk := sessions[bare]
|
|
if !sessionOk || !session.Session.MUC {
|
|
iqAnswerSetError(answer, 403)
|
|
return
|
|
}
|
|
|
|
toID, toOk, toIsGroup := toToID(iq.To)
|
|
if !toOk || !toIsGroup {
|
|
iqAnswerSetError(answer, 406)
|
|
return
|
|
}
|
|
|
|
if len(query.Items) != 1 {
|
|
iqAnswerSetError(answer, 400)
|
|
return
|
|
}
|
|
item := query.Items[0]
|
|
|
|
var membersList telegram.MembersList
|
|
switch item.Role {
|
|
case "moderator":
|
|
membersList = telegram.MembersListAdministrators
|
|
case "participant":
|
|
membersList = telegram.MembersListMembers
|
|
}
|
|
switch item.Affiliation {
|
|
case "owner":
|
|
iqAnswerSetError(answer, 403)
|
|
return
|
|
case "admin":
|
|
membersList = telegram.MembersListAdministrators
|
|
case "member":
|
|
membersList = telegram.MembersListMembers
|
|
case "outcast":
|
|
membersList = telegram.MembersListBanned
|
|
}
|
|
|
|
if membersList == telegram.MembersListNone {
|
|
iqAnswerSetError(answer, 400)
|
|
return
|
|
}
|
|
|
|
payload := &extensions.QueryMucAdmin{}
|
|
answer.Payload = payload
|
|
|
|
members, err := session.GetChatMembers(toID, false, "", membersList)
|
|
if err == nil {
|
|
for _, member := range members {
|
|
senderId, nickname, affiliation, role := session.TgMemberToMUCOccupant(member)
|
|
if item.Role != "" && role != item.Role {
|
|
continue
|
|
}
|
|
if item.Affiliation != "" && affiliation != item.Affiliation {
|
|
continue
|
|
}
|
|
payload.Items = append(payload.Items, &extensions.QueryMucAdminItem{
|
|
Jid: gateway.CHATJID(senderId, true),
|
|
Nick: nickname,
|
|
Role: role,
|
|
Affiliation: affiliation,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) {
|
|
component, answer, ok := iqResultStub(s, iq)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
_, toOk, _ := toToID(iq.To)
|
|
if toOk {
|
|
iqAnswerRegisterSetError(answer, query, 400)
|
|
return
|
|
}
|
|
|
|
if query.Remove != nil {
|
|
iqAnswerRegisterSetError(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 {
|
|
iqAnswerRegisterSetError(answer, query, 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
err := session.TryLogin(resource, query.Username)
|
|
if err != nil {
|
|
if err.Error() == telegram.TelegramAuthDone {
|
|
iqAnswerRegisterSetError(answer, query, 406)
|
|
} else {
|
|
iqAnswerRegisterSetError(answer, query, 500)
|
|
}
|
|
return
|
|
}
|
|
|
|
err = session.SetPhoneNumber(query.Username)
|
|
if err != nil {
|
|
iqAnswerRegisterSetError(answer, query, 500)
|
|
return
|
|
}
|
|
|
|
// everything okay, the response should be empty with no payload/error at this point
|
|
gateway.SubscribeToTransport(component, iq.From)
|
|
} else {
|
|
iqAnswerRegisterSetError(answer, query, 406)
|
|
}
|
|
}
|
|
|
|
func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command) {
|
|
component, answer, ok := iqResultStub(s, iq)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
cancelSend := false
|
|
|
|
defer func() {
|
|
if !cancelSend {
|
|
gateway.ResumableSend(component, answer)
|
|
}
|
|
}()
|
|
|
|
log.Debugf("command: %#v", command)
|
|
|
|
bare, resource, ok := gateway.SplitJID(iq.From)
|
|
if !ok {
|
|
return
|
|
}
|
|
toId, toOk, _ := toToID(iq.To)
|
|
|
|
var cmdString string
|
|
var cmdType telegram.CommandType
|
|
var form *stanza.Form
|
|
for _, ce := range command.CommandElements {
|
|
fo, formOk := ce.(*stanza.Form)
|
|
if formOk {
|
|
form = fo
|
|
break
|
|
}
|
|
}
|
|
if toOk {
|
|
cmdType = telegram.CommandTypeChat
|
|
} else {
|
|
cmdType = telegram.CommandTypeTransport
|
|
}
|
|
if form != nil {
|
|
if command.Node == "config" {
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
var infoStrings []string
|
|
var warnString, errString string
|
|
for _, field := range form.Fields {
|
|
if len(field.ValuesList) > 0 {
|
|
fieldValue := field.ValuesList[0]
|
|
|
|
if gateway.MessageOutgoingPermissionVersion == 0 && field.Var == "carbons" && fieldValue == "true" {
|
|
warnString = "The server did not allow to enable carbons"
|
|
continue
|
|
}
|
|
|
|
// 10. In accordance with Section 3.2.2.1 of XML Schema Part 2: Datatypes, the allowable
|
|
// lexical representations for the xs:boolean datatype are the strings "0" and "false"
|
|
// for the concept 'false' and the strings "1" and "true" for the concept 'true';
|
|
// implementations MUST support both styles of lexical representation.
|
|
if persistence.PropertyType(field.Var) == persistence.PropertyTypeBool {
|
|
if fieldValue == "0" {
|
|
fieldValue = "false"
|
|
}
|
|
if fieldValue == "1" {
|
|
fieldValue = "true"
|
|
}
|
|
}
|
|
|
|
oldValue, err := session.Session.Get(field.Var)
|
|
if err != nil || oldValue != fieldValue {
|
|
value, err := session.Session.Set(field.Var, fieldValue)
|
|
if err != nil {
|
|
errString = fmt.Sprintf("Error for field %v: %v, aborting", field.Var, err.Error())
|
|
break
|
|
}
|
|
if field.Var == "muc" && fieldValue == "true" {
|
|
go session.MigrateToMUCs()
|
|
}
|
|
infoStrings = append(infoStrings, fmt.Sprintf("%s set to %s", field.Var, value))
|
|
gateway.DirtySessions = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var elements []stanza.CommandElement
|
|
if errString != "" {
|
|
elements = append(elements, &stanza.Note{
|
|
Text: errString,
|
|
Type: stanza.CommandNoteTypeErr,
|
|
})
|
|
}
|
|
if warnString != "" {
|
|
elements = append(elements, &stanza.Note{
|
|
Text: warnString,
|
|
Type: stanza.CommandNoteTypeWarn,
|
|
})
|
|
}
|
|
for _, infoString := range infoStrings {
|
|
elements = append(elements, &stanza.Note{
|
|
Text: infoString,
|
|
Type: stanza.CommandNoteTypeInfo,
|
|
})
|
|
}
|
|
|
|
answer.Payload = &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
Status: stanza.CommandStatusCompleted,
|
|
CommandElements: elements,
|
|
}
|
|
}
|
|
} else if !toOk && command.Node == "loginwizard" {
|
|
var session *telegram.Client
|
|
answer.Payload, cancelSend, session = loginWizardPayload(bare, form, resource)
|
|
|
|
log.Debugf("immediate loginwizard payload: %#v", answer.Payload)
|
|
if cancelSend {
|
|
go sendLoginWizardResponse(component, answer, session)
|
|
}
|
|
} else if toOk && command.Node == "botmenu" {
|
|
payload := &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
}
|
|
answer.Payload = payload
|
|
|
|
if len(form.Fields) == 1 && form.Fields[0] != nil &&
|
|
form.Fields[0].Var == "command" && len(form.Fields[0].ValuesList) == 1 {
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
msgText := "/" + form.Fields[0].ValuesList[0]
|
|
session.LastBotCmdString = msgText
|
|
tgMessage := session.ProcessOutgoingMessage(toId, msgText, iq.From, 0, 0, false, true)
|
|
if tgMessage != nil {
|
|
payload.Status = stanza.CommandStatusCompleted
|
|
} else {
|
|
setCommandPayloadError(payload, "Failed to send a bot command")
|
|
}
|
|
} else {
|
|
setCommandPayloadError(payload, "Session is lost")
|
|
}
|
|
} else {
|
|
setCommandPayloadError(payload, "Broken form")
|
|
}
|
|
} else {
|
|
// 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(cmdType, command.Node)
|
|
if ok && len(cmd.Arguments) > 0 {
|
|
var fields []*stanza.Field
|
|
if command.Node == "config" {
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
for _, key := range persistence.ConfigKeys {
|
|
// no reason to display the item if carbons won't work
|
|
if key == "carbons" && gateway.MessageOutgoingPermissionVersion == 0 {
|
|
continue
|
|
}
|
|
|
|
value, err := session.Session.Get(key)
|
|
if err != nil {
|
|
log.Errorf("Achtung! Programming error in sessions with key %v", key)
|
|
continue
|
|
}
|
|
|
|
var fieldType string
|
|
if persistence.PropertyType(key) == persistence.PropertyTypeBool {
|
|
fieldType = stanza.FieldTypeBool
|
|
}
|
|
|
|
field := stanza.Field{
|
|
Var: key,
|
|
Label: key,
|
|
Type: fieldType,
|
|
ValuesList: []string{value},
|
|
}
|
|
fields = append(fields, &field)
|
|
log.Debugf("field: %#v", field)
|
|
}
|
|
}
|
|
} else {
|
|
for i, arg := range cmd.Arguments {
|
|
var required *string
|
|
if i < cmd.RequiredArgs {
|
|
dummyString := ""
|
|
required = &dummyString
|
|
}
|
|
|
|
var fieldType string
|
|
var options []stanza.Option
|
|
if toOk && i == 0 {
|
|
switch command.Node {
|
|
case "mute", "kick", "ban", "promote", "unmute", "unban":
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
var membersList telegram.MembersList
|
|
switch command.Node {
|
|
case "unmute":
|
|
membersList = telegram.MembersListRestricted
|
|
case "unban":
|
|
membersList = telegram.MembersListBannedAndAdministrators
|
|
}
|
|
members, err := session.GetChatMembers(toId, true, "", membersList)
|
|
if err == nil {
|
|
fieldType = stanza.FieldTypeListSingle
|
|
switch command.Node {
|
|
// allow empty form
|
|
case "mute", "unmute":
|
|
options = append(options, stanza.Option{
|
|
ValuesList: []string{""},
|
|
})
|
|
}
|
|
for _, member := range members {
|
|
senderId := session.GetSenderId(member.MemberId)
|
|
options = append(options, stanza.Option{
|
|
Label: session.FormatContact(senderId),
|
|
ValuesList: []string{strconv.FormatInt(senderId, 10)},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
field := stanza.Field{
|
|
Var: strconv.FormatInt(int64(i), 10),
|
|
Label: arg,
|
|
Required: required,
|
|
Type: fieldType,
|
|
Options: options,
|
|
}
|
|
fields = append(fields, &field)
|
|
log.Debugf("field: %#v", field)
|
|
}
|
|
}
|
|
form := stanza.Form{
|
|
Type: stanza.FormTypeForm,
|
|
Title: command.Node,
|
|
Instructions: []string{cmd.Description},
|
|
Fields: fields,
|
|
}
|
|
answer.Payload = &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
Status: stanza.CommandStatusExecuting,
|
|
CommandElements: []stanza.CommandElement{&form},
|
|
}
|
|
log.Debugf("form: %#v", form)
|
|
} else if !toOk && command.Node == "loginwizard" {
|
|
var session *telegram.Client
|
|
answer.Payload, cancelSend, session = loginWizardPayload(bare, nil, resource)
|
|
|
|
log.Debugf("immediate loginwizard payload: %#v", answer.Payload)
|
|
if cancelSend {
|
|
go sendLoginWizardResponse(component, answer, session)
|
|
}
|
|
} else if toOk && command.Node == "botmenu" {
|
|
session, ok := sessions[bare]
|
|
|
|
var link *telegram.BotLink
|
|
var commands []*telegram.BotCommand
|
|
var err error
|
|
if ok {
|
|
link, commands, err = session.GetBotMenu(toId)
|
|
}
|
|
|
|
payload := &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
}
|
|
answer.Payload = payload
|
|
|
|
if !ok || err != nil {
|
|
setCommandPayloadError(payload, "Cannot retrieve commands")
|
|
} else {
|
|
if link != nil {
|
|
payload.Status = stanza.CommandStatusCompleted
|
|
payload.CommandElements = []stanza.CommandElement{
|
|
&stanza.Note{
|
|
Text: fmt.Sprintf("%v: %v", link.Description, link.Link),
|
|
Type: stanza.CommandNoteTypeInfo,
|
|
},
|
|
}
|
|
} else {
|
|
var options []stanza.Option
|
|
for _, cmd := range commands {
|
|
options = append(options, stanza.Option{
|
|
Label: fmt.Sprintf("/%v — %v", cmd.Command, cmd.Description),
|
|
ValuesList: []string{cmd.Command},
|
|
})
|
|
}
|
|
|
|
dummyString := ""
|
|
field := stanza.Field{
|
|
Var: "command",
|
|
Type: stanza.FieldTypeListSingle,
|
|
Required: &dummyString,
|
|
Options: options,
|
|
}
|
|
log.Debugf("field: %#v", field)
|
|
|
|
form := stanza.Form{
|
|
Type: stanza.FormTypeForm,
|
|
Fields: []*stanza.Field{&field},
|
|
}
|
|
log.Debugf("form: %#v", form)
|
|
|
|
payload.Status = stanza.CommandStatusExecuting
|
|
payload.CommandElements = []stanza.CommandElement{&form}
|
|
}
|
|
}
|
|
} else {
|
|
cmdString = "/" + command.Node
|
|
}
|
|
} else if command.Action == stanza.CommandActionCancel {
|
|
if command.Node == "loginwizard" {
|
|
session, ok := sessions[bare]
|
|
if ok {
|
|
session.ProcessTransportCommand("/cancelauth", resource)
|
|
}
|
|
}
|
|
answer.Payload = &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
Status: stanza.CommandStatusCancelled,
|
|
}
|
|
}
|
|
}
|
|
|
|
if cmdString != "" {
|
|
session, ok := sessions[bare]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var response string
|
|
var success bool
|
|
if toOk {
|
|
response, _, success = session.ProcessChatCommand(toId, cmdString)
|
|
} else {
|
|
response, success = session.ProcessTransportCommand(cmdString, resource)
|
|
}
|
|
|
|
var noteType string
|
|
if success {
|
|
noteType = stanza.CommandNoteTypeInfo
|
|
} else {
|
|
noteType = stanza.CommandNoteTypeErr
|
|
}
|
|
|
|
answer.Payload = &stanza.Command{
|
|
SessionId: command.Node,
|
|
Node: command.Node,
|
|
Status: stanza.CommandStatusCompleted,
|
|
CommandElements: []stanza.CommandElement{
|
|
&stanza.Note{
|
|
Text: response,
|
|
Type: noteType,
|
|
},
|
|
},
|
|
}
|
|
|
|
}
|
|
|
|
log.Debugf("command response: %#v %v", answer.Payload, cancelSend)
|
|
}
|
|
|
|
func handleSetQueryMucAdmin(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucAdmin) {
|
|
component, answer, ok := iqResultStub(s, iq)
|
|
if !ok {
|
|
return
|
|
}
|
|
defer gateway.ResumableSend(component, answer)
|
|
|
|
bare, _, fromOk := gateway.SplitJID(iq.From)
|
|
if !fromOk {
|
|
iqAnswerSetError(answer, 400)
|
|
return
|
|
}
|
|
|
|
session, sessionOk := sessions[bare]
|
|
if !sessionOk || !session.Session.MUC {
|
|
iqAnswerSetError(answer, 403)
|
|
return
|
|
}
|
|
|
|
toID, toOk, toIsGroup := toToID(iq.To)
|
|
if !toOk || !toIsGroup {
|
|
iqAnswerSetError(answer, 406)
|
|
return
|
|
}
|
|
|
|
// pre-bake all data to make it transactional as much as possible
|
|
type Item struct {
|
|
UserID int64
|
|
Nick string
|
|
Status telegram.ChatMemberStatus
|
|
}
|
|
|
|
var items []Item
|
|
for _, item := range query.Items {
|
|
if item.Affiliation == "owner" {
|
|
iqAnswerSetError(answer, 403)
|
|
return
|
|
}
|
|
|
|
var userID int64
|
|
if item.Jid != "" {
|
|
userID, _, _ = toToID(item.Jid)
|
|
} else if item.Nick != "" {
|
|
userID = session.GetMUCMemberIdByNickname(toID, item.Nick)
|
|
}
|
|
if userID == 0 {
|
|
iqAnswerSetError(answer, 404)
|
|
return
|
|
}
|
|
|
|
nick := session.GetMUCNickname(userID)
|
|
|
|
var status telegram.ChatMemberStatus
|
|
|
|
switch item.Role {
|
|
case "none":
|
|
status = telegram.ChatMemberStatusKicked
|
|
case "visitor":
|
|
status = telegram.ChatMemberStatusMuted
|
|
case "participant":
|
|
status = telegram.ChatMemberStatusUnmuted
|
|
case "moderator":
|
|
status = telegram.ChatMemberStatusPromoted
|
|
}
|
|
// affiliations have a higher priority over roles
|
|
switch item.Affiliation {
|
|
case "none":
|
|
status = telegram.ChatMemberStatusKicked
|
|
case "outcast":
|
|
status = telegram.ChatMemberStatusBanned
|
|
case "member":
|
|
status = telegram.ChatMemberStatusUnmuted
|
|
case "admin":
|
|
status = telegram.ChatMemberStatusPromoted
|
|
}
|
|
|
|
// nothing has been detected
|
|
if status == telegram.ChatMemberStatusIllegal {
|
|
iqAnswerSetError(answer, 400)
|
|
return
|
|
}
|
|
|
|
items = append(items, Item{
|
|
UserID: userID,
|
|
Nick: nick,
|
|
Status: status,
|
|
})
|
|
}
|
|
|
|
for _, item := range items {
|
|
err := session.SetChatMemberStatus(toID, item.UserID, item.Status, 0, "", item.Nick)
|
|
if err != nil {
|
|
code, ok := telegram.GetErrorCode(err)
|
|
if !ok {
|
|
code = 500
|
|
}
|
|
iqAnswerSetError(answer, int(code))
|
|
answer.Error.Text = err.Error()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func iqAnswerSetError(answer *stanza.IQ, code int) {
|
|
iqAnswerSetErrorInternal(answer, code, false)
|
|
}
|
|
|
|
func iqAnswerRegisterSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) {
|
|
answer.Payload = *payload
|
|
iqAnswerSetErrorInternal(answer, code, true)
|
|
}
|
|
|
|
func iqAnswerSetErrorInternal(answer *stanza.IQ, code int, registerMode bool) {
|
|
answer.Type = stanza.IQTypeError
|
|
switch code {
|
|
case 400:
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeModify,
|
|
Reason: "bad-request",
|
|
}
|
|
case 401:
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeAuth,
|
|
Reason: "not-authorized",
|
|
}
|
|
case 403:
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeAuth,
|
|
Reason: "forbidden",
|
|
}
|
|
case 404:
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeCancel,
|
|
Reason: "item-not-found",
|
|
}
|
|
case 405:
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeCancel,
|
|
Reason: "not-allowed",
|
|
}
|
|
case 406:
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeModify,
|
|
Reason: "not-acceptable",
|
|
}
|
|
case 500:
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeWait,
|
|
Reason: "internal-server-error",
|
|
}
|
|
default:
|
|
log.Error("Unknown error code, falling back with empty reason")
|
|
answer.Error = &stanza.Err{
|
|
Type: stanza.ErrorTypeCancel,
|
|
Reason: "undefined-condition",
|
|
}
|
|
}
|
|
answer.Error.Code = code
|
|
|
|
if registerMode {
|
|
switch code {
|
|
case 404:
|
|
answer.Error.Text = "No such room"
|
|
case 405:
|
|
answer.Error.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.Text = "Phone number already provided, chat with the transport for further instruction"
|
|
}
|
|
}
|
|
}
|
|
|
|
func presenceReplySetError(reply *stanza.Presence, code int) {
|
|
reply.Type = stanza.PresenceTypeError
|
|
reply.Error = stanza.Err{
|
|
Code: code,
|
|
}
|
|
switch code {
|
|
case 400:
|
|
reply.Error.Type = stanza.ErrorTypeModify
|
|
reply.Error.Reason = "jid-malformed"
|
|
case 407:
|
|
reply.Error.Type = stanza.ErrorTypeAuth
|
|
reply.Error.Reason = "registration-required"
|
|
case 404:
|
|
reply.Error.Type = stanza.ErrorTypeCancel
|
|
reply.Error.Reason = "item-not-found"
|
|
default:
|
|
log.Error("Unknown error code, falling back with empty reason")
|
|
reply.Error.Type = stanza.ErrorTypeCancel
|
|
reply.Error.Reason = "undefined-condition"
|
|
}
|
|
}
|
|
|
|
func setCommandPayloadError(payload *stanza.Command, err string) {
|
|
note := stanza.Note{
|
|
Text: err,
|
|
Type: stanza.CommandNoteTypeErr,
|
|
}
|
|
payload.Status = stanza.CommandStatusCompleted
|
|
payload.CommandElements = append(payload.CommandElements, ¬e)
|
|
}
|
|
|
|
func probeClientFeatures(jid string, component *xmpp.Component) {
|
|
id, err := uuid.NewRandom()
|
|
if err != nil {
|
|
log.Error("Could not generate ID for a client features probe")
|
|
return
|
|
}
|
|
|
|
probe := stanza.IQ{
|
|
Attrs: stanza.Attrs{
|
|
From: gateway.Jid.Bare(),
|
|
To: jid,
|
|
Id: id.String(),
|
|
Type: stanza.IQTypeGet,
|
|
},
|
|
Payload: &stanza.DiscoInfo{},
|
|
}
|
|
log.Debugf("%#v", probe)
|
|
|
|
gateway.ResumableSend(component, &probe)
|
|
}
|
|
|
|
func handleClientFeatures(s xmpp.Sender, iq *stanza.IQ, discoInfo *stanza.DiscoInfo) {
|
|
fromJid, err := stanza.NewJid(iq.From)
|
|
if err != nil {
|
|
log.Error("Invalid from JID!")
|
|
return
|
|
}
|
|
bareFrom := fromJid.Bare()
|
|
|
|
session, ok := sessions[bareFrom]
|
|
if !ok {
|
|
log.Errorf("Got client features for unknown JID %v", bareFrom)
|
|
return
|
|
}
|
|
|
|
var features []string
|
|
var avatarNotify bool
|
|
for _, feature := range discoInfo.Features {
|
|
features = append(features, feature.Var)
|
|
if feature.Var == gateway.NodeAvatarMetadataNotify {
|
|
avatarNotify = true
|
|
}
|
|
}
|
|
|
|
session.XmppClientFeaturesLock.Lock()
|
|
session.XmppClientFeatures[fromJid.Resource] = &features
|
|
session.XmppClientFeaturesLock.Unlock()
|
|
|
|
log.Debugf("Features for %v: %#v", iq.From, features)
|
|
|
|
if avatarNotify {
|
|
go sendPubSubAvatarNotifications(s, iq.From, session)
|
|
}
|
|
}
|
|
|
|
func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram.Client) {
|
|
component, ok := s.(*xmpp.Component)
|
|
if !ok {
|
|
log.Error("Not a component")
|
|
return
|
|
}
|
|
|
|
for _, chatId := range session.ChatsKeys() {
|
|
chat, _, err := session.GetContactByID(chatId, nil)
|
|
if err != nil || chat == nil {
|
|
continue
|
|
}
|
|
|
|
if chat.Photo == nil {
|
|
session.SetEmptyAvatarHash(chatId)
|
|
continue
|
|
}
|
|
|
|
sha1 := session.GetPhotoSha1(chat.Photo.Small, chat.Id)
|
|
size := session.GetPhotoSize(chat.Photo.Small)
|
|
|
|
gateway.SendPubSubAvatarNotification(component, jid, chat.Id, sha1, size)
|
|
}
|
|
}
|
|
|
|
func toToID(to string) (int64, bool, bool) {
|
|
var isGroup bool
|
|
toParts := strings.Split(to, "@")
|
|
if len(toParts) < 2 {
|
|
return 0, false, isGroup
|
|
}
|
|
node := toParts[0]
|
|
if strings.HasPrefix(node, "c") {
|
|
isGroup = true
|
|
node = node[1:]
|
|
}
|
|
toID, err := strconv.ParseInt(node, 10, 64)
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"to": to,
|
|
}).Error(errors.Wrap(err, "Invalid to JID!"))
|
|
return 0, false, isGroup
|
|
}
|
|
return toID, true, isGroup
|
|
}
|
|
|
|
func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload {
|
|
var base64Photo string
|
|
if info.Photo != nil {
|
|
base64Photo = session.GetPhotoBase64(info.Photo)
|
|
}
|
|
|
|
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: gateway.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
|
|
}
|