telegabber/xmpp/handlers.go
2026-06-02 21:43:08 -04:00

3171 lines
78 KiB
Go

package xmpp
import (
"encoding/xml"
"fmt"
"github.com/pkg/errors"
"sort"
"strconv"
"strings"
"sync"
"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
)
const MUC_DEFAULT_HISTORY_LIMIT int32 = 20
// kludge for client resuming after the transport restart (by full jids)
var probeFired = make(map[string]bool)
var probeFiredLock = sync.Mutex{}
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
}
_, ok = iq.Payload.(*extensions.QueryMucOwner)
if ok {
go handleGetQueryMucOwner(s, iq)
return
}
queryMAM2, ok := iq.Payload.(*extensions.MAM2Query)
if ok {
go handleGetQueryMAM(s, iq, queryMAM2)
return
}
queryMAM1, ok := iq.Payload.(*extensions.MAM1Query)
if ok {
go handleGetQueryMAM(s, iq, queryMAM1)
return
}
queryMAM0, ok := iq.Payload.(*extensions.MAM0Query)
if ok {
go handleGetQueryMAM(s, iq, queryMAM0)
return
}
_, ok = iq.Payload.(*extensions.MAM2Metadata)
if ok {
go handleGetMetadataMAM2(s, iq)
return
}
_, ok = iq.Payload.(*stanza.Version)
if ok {
go handleGetVersion(s, iq)
return
}
_, ok = iq.Payload.(*extensions.EntityTime)
if ok {
go handleGetEntityTime(s, iq)
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
}
queryMucOwner, ok := iq.Payload.(*extensions.QueryMucOwner)
if ok {
go handleSetQueryMucOwner(s, iq, queryMucOwner)
return
}
queryMAM2, ok := iq.Payload.(*extensions.MAM2Query)
if ok {
go handleSetQueryMAM(s, iq, queryMAM2)
return
}
queryMAM1, ok := iq.Payload.(*extensions.MAM1Query)
if ok {
go handleSetQueryMAM(s, iq, queryMAM1)
return
}
queryMAM0, ok := iq.Payload.(*extensions.MAM0Query)
if ok {
go handleSetQueryMAM(s, iq, queryMAM0)
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, true)
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
var attachTo extensions.AttachTo
msg.Get(&reply)
msg.Get(&fallback)
msg.Get(&replace)
msg.Get(&attachTo)
log.Debugf("reply: %#v", reply)
log.Debugf("fallback: %#v", fallback)
log.Debugf("replace: %#v", replace)
log.Debugf("attachTo: %#v", attachTo)
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 {
replyId, _ = parseMessageId(reply.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, isCommand := 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 {
messageId := msg.Id
if attachTo.Id != "" {
messageId = attachTo.Id
}
err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessage.Id, messageId)
if err == nil {
// session.AddToOutbox(msg.Id, resource)
session.UpdateLastChatMessageId(toID, messageId)
} else {
log.Errorf("Failed to save ids %v/%v %v", toID, tgMessage.Id, messageId)
}
}
// 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 isCommand && isGroupchat && session.Session.MUC {
// pong outgoing commands back to groupchats
gateway.SendMessage(msg.From, msg.To + "/" + session.GetMUCNickname(0), text, "", component, nil, 0, "", false, isGroupchat, false, "", "", "", "", nil)
} 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, session := range sessions {
if strings.HasSuffix(bare, suffix) {
for _, jid := range session.GetCarbonFullJids(true, "", false) {
gateway.SendServiceMessage(jid, "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, "", "", true, 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, false)
if err != nil {
log.Error(errors.Wrap(err, "TDlib connection failure"))
} else {
var probeFiredForFrom bool
probeFiredLock.Lock()
_, probeFiredForFrom = probeFired[p.From]
probeFired[p.From] = true
probeFiredLock.Unlock()
if p.Type != "probe" || !probeFiredForFrom {
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,
true,
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, 405)
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, true)
if err != nil || !session.IsGroup(chat) {
presenceReplySetError(reply, 404)
return
}
status := session.GetMyStatusInChat(chatId)
log.Debugf("status in group %v: %v", chatId, status)
switch status {
case telegram.ChatMemberStatusBanned, telegram.ChatMemberStatusIllegal:
presenceReplySetError(reply, 403)
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(MUC_DEFAULT_HISTORY_LIMIT)
}
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, true)
if err != nil || !session.IsGroup(chat) {
return
}
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
if p.Type == stanza.PresenceTypeError {
return
}
if !session.MUCHasResource(chatId, fromResource) {
// groupchat 1.0 join
gateway.SendPresence(
component,
p.From,
gateway.SPFullFrom(p.To),
gateway.SPType("unavailable"),
gateway.SPMUCAffiliation("none"),
gateway.SPMUCRole("none"),
gateway.SPMUCStatusCodes([]uint16{110, 307, 333}),
)
return
}
switch p.Type {
case "":
if p.Show == "" {
handleMUCNicknameChange(component, p, session, chatId, toBare, nickname)
}
case stanza.PresenceTypeUnavailable:
handleMUCUnavailable(component, p, session, chatId, fromResource)
}
}
func handleMUCNicknameChange(component *xmpp.Component, p stanza.Presence, session *telegram.Client, chatId int64, toBare string, newNickname string) {
log.Warn("🗿 Yes")
from := toBare
nickname, ok := session.GetMyMUCNickname(chatId)
if ok {
if nickname == newNickname {
log.Warn("But whatever, it's the same")
return
}
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)
gateway.SendPresence(
component,
p.From,
gateway.SPFullFrom(p.To),
gateway.SPType("unavailable"),
gateway.SPMUCAffiliation("member"),
gateway.SPMUCRole("none"),
gateway.SPMUCJid(p.From),
gateway.SPMUCStatusCodes([]uint16{110}),
)
}
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
}
toID, toOk, isGroup := toToID(iq.To)
if !toOk {
log.Error("Invalid IQ to")
return
}
if isGroup {
toJid, err := stanza.NewJid(iq.To)
if err != nil {
log.Error("Invalid to JID!")
return
}
if toJid.Resource != "" {
toID = session.GetMUCMemberIdByNickname(toID, toJid.Resource)
}
}
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 := session.GetHashedAvatar(chatId)
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, true)
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()
answer.Payload = disco
toID, toOk, toIsGroup := toToID(iq.To)
if toIsGroup {
toJid, err := stanza.NewJid(iq.To)
if err == nil && toJid.Resource != "" {
iqAnswerSetError(answer, 400)
return
}
}
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, true)
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_moderated",
"muc_nonanonymous",
"muc_unsecured",
"http://jabber.org/protocol/muc#stable_id",
"jabber:iq:register",
gateway.NS_MAM0,
gateway.NS_MAM1,
gateway.NS_MAM2,
"urn:xmpp:mam:2#extended",
"urn:xmpp:sid:0",
"vcard-temp",
)
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)},
},
}
hashedAvatar := session.GetHashedAvatar(toID)
if hashedAvatar != nil {
fields = append(fields, &stanza.Field{
Var: "muc#roominfo_avatarhash",
Label: "Avatar hash",
ValuesList: []string{hashedAvatar.Hash},
})
}
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 toIsGroup {
if !isMuc {
iqAnswerSetError(answer, 404)
return
}
} else {
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)
disco.AddFeatures("jabber:iq:version")
disco.AddFeatures("urn:xmpp:time")
} 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, true)
if err == nil && session.IsGroup(chat) {
disco.SetNode(di.Node)
disco.AddIdentity(session.GetMUCNickname(0), "conference", "text")
}
}
}
}
} else if di.Node == "http://jabber.org/protocol/muc#traffic" {
// noop yet, empty result as intended, TODO: add XHTML whenever supported
} 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
}
}
}
}
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, toIsGroup := toToID(iq.To)
if toIsGroup {
toJid, err := stanza.NewJid(iq.To)
if err == nil && toJid.Resource != "" {
iqAnswerSetError(answer, 400)
return
}
}
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, true)
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, telegram.OnlineFilterAny) {
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 handleGetVersion(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
}
answer.Version().SetInfo(gateway.Jid.Resource, gateway.Version, "")
log.Debugf("%#v", answer.Payload)
_ = gateway.ResumableSend(component, answer)
}
func handleGetEntityTime(s xmpp.Sender, iq *stanza.IQ) {
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
// separate declaration is crucial for passing as pointer to defer
var answer *stanza.IQ
var err error
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)
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
}
entityTime := extensions.EntityTime{
Tzo: session.GetTZD(),
Utc: time.Now().UTC().Format(time.RFC3339),
}
answer.Payload = &entityTime
log.Debugf("%#v", entityTime)
}
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
}
chat, _, err := session.GetContactByID(toID, nil, true)
if err != nil || !session.IsGroup(chat) {
iqAnswerSetError(answer, 405)
return
}
if len(query.Items) != 1 {
iqAnswerSetError(answer, 400)
return
}
item := query.Items[0]
if item.Role != "" && item.Affiliation != "" {
iqAnswerSetError(answer, 400)
return
}
var membersList telegram.MembersList
switch item.Role {
case "moderator":
membersList = telegram.MembersListAdministrators
case "participant":
membersList = telegram.MembersListMembers
}
switch item.Affiliation {
case "owner":
membersList = telegram.MembersListCreators
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, chat)
if item.Role != "" && role != item.Role {
continue
}
if item.Affiliation != "" && affiliation != item.Affiliation {
continue
}
newItem := extensions.QueryMucAdminItem{}
if item.Affiliation == "" || item.Affiliation != "outcast" {
newItem.Nick = nickname
}
if item.Role != "" {
newItem.Role = role
newItem.Jid = gateway.CHATJID(senderId, true)
} else {
newItem.Affiliation = affiliation
newItem.Jid = gateway.CHATJID(senderId, false)
}
payload.Items = append(payload.Items, &newItem)
}
}
}
func handleGetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ) {
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, 405)
return
}
chat, _, err := session.GetContactByID(toID, nil, true)
if err != nil || chat == nil || !session.IsGroup(chat) {
iqAnswerSetError(answer, 405)
return
}
payload := &extensions.QueryMucOwner{}
answer.Payload = payload
dummyString := ""
changeSubject := "1"
if chat.Permissions != nil && !chat.Permissions.CanPinMessages {
changeSubject = "0"
}
payload.Form = &stanza.Form{
Type: stanza.FormTypeForm,
Title: fmt.Sprintf("Configuration for \"%v\" room", chat.Title),
Fields: []*stanza.Field{
&stanza.Field{
Var: "FORM_TYPE",
Type: stanza.FieldTypeHidden,
ValuesList: []string{"http://jabber.org/protocol/muc#roominfo"},
},
&stanza.Field{
Var: "muc#roomconfig_roomname",
Label: "Group name",
Required: &dummyString,
ValuesList: []string{chat.Title},
},
&stanza.Field{
Var: "muc#roomconfig_roomdesc",
Label: "Description (optional)",
ValuesList: []string{session.GetChatDescription(chat)},
},
&stanza.Field{
Var: "muc#roomconfig_changesubject",
Label: "Allow Occupants to Change Subject?",
Type: stanza.FieldTypeBool,
ValuesList: []string{changeSubject},
},
},
}
}
func handleGetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) {
component, answer, ok := iqResultStub(s, iq)
if !ok {
return
}
defer gateway.ResumableSend(component, answer)
payload2 := &extensions.MAM2Query{}
payload1 := &extensions.MAM1Query{}
payload0 := &extensions.MAM0Query{}
ns := query.Namespace()
switch ns {
case gateway.NS_MAM2:
answer.Payload = payload2
case gateway.NS_MAM1:
answer.Payload = payload1
case gateway.NS_MAM0:
answer.Payload = payload0
}
form := stanza.Form{
Type: stanza.FormTypeForm,
Fields: []*stanza.Field{
&stanza.Field{
Var: "FORM_TYPE",
Type: stanza.FieldTypeHidden,
ValuesList: []string{ns},
},
&stanza.Field{
Var: "with",
Type: stanza.FieldTypeJidSingle,
},
&stanza.Field{
Var: "start",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "end",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "before-id",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "after-id",
Type: stanza.FieldTypeTextSingle,
},
&stanza.Field{
Var: "ids",
Type: stanza.FieldTypeListMulti,
},
},
}
payload2.Form = &form
payload1.Form = &form
payload0.Form = &form
log.Debugf("MAM info request: %#v", query)
}
func handleGetMetadataMAM2(s xmpp.Sender, iq *stanza.IQ) {
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, 405)
return
}
chat, _, err := session.GetContactByID(toID, nil, true)
if err != nil || chat == nil || !session.IsGroup(chat) {
iqAnswerSetError(answer, 405)
return
}
payload := &extensions.MAM2Metadata{}
answer.Payload = payload
quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold))
preFirstMessage, preFirstMessageErr := session.FindMessageByTime(toID, quotaTs)
var preFirstMessageId int64
if preFirstMessageErr == nil && preFirstMessage != nil {
preFirstMessageId = preFirstMessage.Id
} else {
preFirstMessageId = 1
}
startMessage, startMessageErr := session.GetNextMessage(toID, preFirstMessageId)
if startMessageErr == nil && startMessage != nil {
payload.Start = &extensions.MAM2MetadataStart{
Id: strconv.FormatInt(startMessage.Id, 10),
Timestamp: extensions.TimestampToRFC3339(int64(startMessage.Date)),
}
}
endMessage, endMessageErr := session.GetPreviousMessage(toID, 0)
if endMessageErr == nil && endMessage != nil {
payload.End = &extensions.MAM2MetadataEnd{
Id: strconv.FormatInt(endMessage.Id, 10),
Timestamp: extensions.TimestampToRFC3339(int64(endMessage.Date)),
}
}
log.Debugf("MAM metadata: %#v", payload)
}
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 {
// 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.
fieldValue := persistence.NormalizeProperty(field.Var, field.ValuesList[0])
if gateway.MessageOutgoingPermissionVersion == 0 && field.Var == "carbons" && fieldValue == "true" {
warnString = "The server did not allow to enable carbons"
continue
}
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" {
switch fieldValue {
case "true":
go session.MigrateToMUCs()
case "false":
go session.MigrateFromMUCs()
}
}
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, command.Action)
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
}
if (item.Role != "" && item.Affiliation != "") ||
(item.Role != "" && item.Nick == "") ||
(item.Affiliation != "" && item.Jid == "") {
iqAnswerSetError(answer, 400)
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
}
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 handleSetQueryMucOwner(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryMucOwner) {
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, 405)
return
}
chat, _, err := session.GetContactByID(toID, nil, true)
if err != nil || chat == nil || !session.IsGroup(chat) {
iqAnswerSetError(answer, 405)
return
}
if query.Form != nil {
switch query.Form.Type {
case stanza.FormTypeSubmit:
// okay, noop
case stanza.FormTypeCancel:
return
default:
iqAnswerSetError(answer, 400)
return
}
err = nil
for _, field := range query.Form.Fields {
switch field.Var {
case "muc#roomconfig_roomname", "muc#roomconfig_roomdesc", "muc#roomconfig_changesubject":
if len(field.ValuesList) != 1 {
iqAnswerSetError(answer, 400)
return
}
value := field.ValuesList[0]
switch field.Var {
case "muc#roomconfig_roomname":
if value == "" {
iqAnswerSetError(answer, 400)
return
}
if value != chat.Title {
err = session.SetChatTitle(toID, value)
if err != nil {
break
}
}
case "muc#roomconfig_roomdesc":
if value != session.GetChatDescription(chat) {
err = session.SetChatDescription(toID, value)
if err != nil {
break
}
}
case "muc#roomconfig_changesubject":
b, ok := ToBool(value)
if !ok {
iqAnswerSetError(answer, 400)
return
}
permissions := chat.Permissions
if permissions != nil && b != permissions.CanPinMessages {
newPermissions := telegram.CloneChatPermissions(permissions)
newPermissions.CanPinMessages = b
err = session.SetChatPermissions(toID, newPermissions)
if err != nil {
break
}
}
}
}
}
} else if query.Destroy != nil {
err = session.DestroyMUC(toID)
} else {
// per 1.0 spec version, it also could be a destruction, but too dangerous to cover probably
iqAnswerSetError(answer, 400)
return
}
if err != nil {
code, ok := telegram.GetErrorCode(err)
if !ok {
code = 500
}
iqAnswerSetError(answer, int(code))
answer.Error.Text = err.Error()
return
}
}
func handleSetQueryMAM(s xmpp.Sender, iq *stanza.IQ, query extensions.MAMQuery) {
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, 405)
return
}
chat, _, err := session.GetContactByID(toID, nil, true)
if err != nil || chat == nil || !session.IsGroup(chat) {
iqAnswerSetError(answer, 405)
return
}
var startTime, endTime time.Time
var beforeId, afterId int64
var ids []string
var rsmBefore, rsmAfter int64
var rsmLastPage bool
var rsmLimit int32
var justCount bool
form := query.GetForm()
queryRs := query.GetSet()
ns := query.Namespace()
log.Debugf("MAM query to %v: %#v %#v %#v", toID, query, form, queryRs)
if form != nil && form.Type == stanza.FormTypeSubmit {
for _, field := range form.Fields {
if len(field.ValuesList) < 1 {
iqAnswerSetError(answer, 400)
return
}
value := field.ValuesList[0]
log.Debugf("MAM query field: %v %v", field.Var, value)
switch field.Var {
case "FORM_TYPE":
if value != ns {
iqAnswerSetError(answer, 400)
return
}
case "with": // okay, and?
case "start", "end":
timestamp, err := time.Parse(time.RFC3339, value)
if err != nil {
iqAnswerSetError(answer, 400)
return
}
switch field.Var {
case "start":
startTime = timestamp
case "end":
endTime = timestamp
}
case "before-id":
beforeId, ok = parseMessageId(value)
if !ok {
iqAnswerSetError(answer, 400)
return
}
case "after-id":
afterId, ok = parseMessageId(value)
if !ok {
iqAnswerSetError(answer, 400)
return
}
case "ids":
ids = field.ValuesList
default:
iqAnswerSetError(answer, 501)
return
}
}
}
if queryRs != nil {
if queryRs.After != nil {
rsmAfter, ok = parseMessageId(*queryRs.After)
if !ok {
iqAnswerSetError(answer, 400)
return
}
log.Debugf("MAM RSM after: %v", rsmAfter)
}
if queryRs.Before != nil {
before := *queryRs.Before
if before == "" {
rsmLastPage = true
log.Debugf("MAM RSM last page")
} else {
rsmBefore, ok = parseMessageId(*queryRs.Before)
if !ok {
iqAnswerSetError(answer, 400)
return
}
log.Debugf("MAM RSM before: %v", rsmBefore)
}
}
if queryRs.Max != nil {
rsmLimit = int32(*queryRs.Max)
if rsmLimit == 0 {
justCount = true
}
log.Debugf("MAM RSM max: %v", rsmLimit)
}
if queryRs.First != nil {
iqAnswerSetError(answer, 400)
return
}
if queryRs.Index != nil {
iqAnswerSetError(answer, 501)
return
}
if queryRs.Last != nil {
iqAnswerSetError(answer, 400)
return
}
}
if rsmLimit == 0 && !justCount {
rsmLimit = 100
}
// check for mutual parameter compatibility, there's a lot of them, nah?
if ((!startTime.IsZero() || !endTime.IsZero()) && (beforeId != 0 || afterId != 0 || ids != nil)) ||
((beforeId != 0 || afterId != 0) && (!startTime.IsZero() || !endTime.IsZero() || ids != nil)) ||
(ids != nil && (!startTime.IsZero() || !endTime.IsZero() || beforeId != 0 || afterId != 0)) {
iqAnswerSetError(answer, 501)
log.Debugf("MAM: incompatible parameters")
return
}
// dummy call to circumvent unexported type
messages, _, err := session.GetMessagesBetween(toID, 0, 0, 0, false)
var order bool // false from start, true from end
// lower priority
if !endTime.IsZero() {
order = true
}
// higher priority
if beforeId != 0 {
order = false
}
if afterId != 0 {
order = true
}
if rsmAfter != 0 {
order = false
}
if rsmBefore != 0 || rsmLastPage {
order = true
}
if order {
rsmLimit = -rsmLimit // hacky, I know, and? :P
}
quotaTs := time.Now().AddDate(0, 0, -int(gateway.MAMThreshold))
var beyond, complete bool
fromStart := !order
toEnd := order
var overallyFirstMessageId, overallyLastMessageId int64
reverse := query.GetFlipPage() != nil
if ids != nil {
for _, sId := range ids {
id, ok := parseMessageId(sId)
if !ok {
iqAnswerSetError(answer, 400)
log.Debugf("MAM: bogus id")
return
}
msg, err := session.GetMessage(toID, id)
if err != nil {
iqAnswerSetError(answer, 404)
log.Debugf("MAM: unknown id")
return
}
messages = append(messages, msg)
}
messages = telegram.ChronologicallySortMessages(messages, reverse)
complete = true
} else if beforeId != 0 || afterId != 0 {
if (beforeId != 0 && afterId != 0) && beforeId < afterId {
iqAnswerSetError(answer, 400)
log.Debugf("MAM: before < after")
return
}
// don't allow to fetch far beyond the quota
var replaceWithQuotaTs bool
if afterId != 0 {
afterMessage, afterMessageErr := session.GetMessage(toID, afterId)
if afterMessageErr == nil && afterMessage != nil {
if int64(afterMessage.Date) < quotaTs.Unix() {
replaceWithQuotaTs = true
}
} else {
iqAnswerSetError(answer, 404)
log.Debugf("MAM: unknown after")
return
}
} else {
replaceWithQuotaTs = true
}
if replaceWithQuotaTs {
preFirstMessage, preFirstMessageErr := session.FindMessageByTime(toID, quotaTs)
if preFirstMessageErr == nil && preFirstMessage != nil {
afterId = preFirstMessage.Id
} else {
// there seem to be no messages older than quota, it's safe to fetch from the very start
afterId = 1 // https://github.com/tdlib/td/issues/195#issuecomment-380836359
}
}
if beforeId != 0 {
beforeMessage, beforeMessageErr := session.GetMessage(toID, beforeId)
if beforeMessageErr != nil || beforeMessage == nil {
iqAnswerSetError(answer, 404)
log.Debugf("MAM: unknown before")
return
}
overallyLastMessage, overallyLastMessageErr := session.GetPreviousMessage(toID, beforeId)
if overallyLastMessageErr == nil && overallyLastMessage != nil {
overallyLastMessageId = overallyLastMessage.Id
}
} else {
newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0)
if newestMessageErr == nil && newestMessage != nil {
overallyLastMessageId = newestMessage.Id
}
}
if rsmAfter != 0 {
if rsmAfter < afterId || (beforeId != 0 && rsmAfter > beforeId) {
iqAnswerSetError(answer, 400)
log.Debugf("MAM: unknown RSM after")
return
}
if rsmAfter != afterId {
fromStart = false
overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, afterId)
if overallyFirstMessageErr == nil && overallyFirstMessage != nil {
overallyFirstMessageId = overallyFirstMessage.Id
}
}
afterId = rsmAfter
}
if rsmBefore != 0 {
if rsmBefore < afterId || (beforeId != 0 && rsmBefore > beforeId) {
iqAnswerSetError(answer, 400)
log.Debugf("MAM: unknown RSM before")
return
}
if rsmBefore < beforeId {
toEnd = false
}
beforeId = rsmBefore
}
var lastMessageId int64
// should be fine even with beforeId=0 as it would mean the last as needed
lastMessage, lastMessageErr := session.GetPreviousMessage(toID, beforeId)
if lastMessageErr == nil && lastMessage != nil {
lastMessageId = lastMessage.Id
} else {
beyond = true
}
if !beyond {
messages, complete, err = session.GetMessagesBetween(toID, afterId, lastMessageId, rsmLimit, reverse)
}
} else { // time limit or no limits at all
// don't allow to fetch far beyond the quota
if !endTime.IsZero() && endTime.Before(quotaTs) {
beyond = true
complete = true
}
if (!startTime.IsZero() && startTime.Before(quotaTs)) || startTime.IsZero() {
startTime = quotaTs
}
if !beyond {
var fromMessageId int64
var lastMessageId int64
fromMessage, fromMessageErr := session.FindMessageByTime(toID, startTime)
if fromMessageErr == nil && fromMessage != nil {
fromMessageId = fromMessage.Id
} else {
// there seem to be no messages older than quota, it's safe to fetch from the very start
fromMessageId = 1 // https://github.com/tdlib/td/issues/195#issuecomment-380836359
}
if !endTime.IsZero() {
endMsg, endMsgErr := session.FindMessageByTime(toID, endTime)
if endMsgErr == nil && endMsg != nil {
lastMessageId = endMsg.Id
overallyLastMessageId = lastMessageId
} else {
beyond = true
complete = true
}
} else {
newestMessage, newestMessageErr := session.GetPreviousMessage(toID, 0)
if newestMessageErr == nil && newestMessage != nil {
overallyLastMessageId = newestMessage.Id
}
}
if rsmAfter != 0 {
rsmAfterMessage, rsmAfterMessageErr := session.GetMessage(toID, rsmAfter)
if rsmAfterMessageErr == nil && rsmAfterMessage != nil {
if !endTime.IsZero() && int64(rsmAfterMessage.Date) > endTime.Unix() {
iqAnswerSetError(answer, 400)
log.Debugf("MAM: RSM after out of range")
return
}
if rsmAfterMessage.Id > fromMessageId && int64(rsmAfterMessage.Date) >= startTime.Unix() {
fromStart = false
overallyFirstMessage, overallyFirstMessageErr := session.GetNextMessage(toID, fromMessageId)
if overallyFirstMessageErr == nil && overallyFirstMessage != nil {
overallyFirstMessageId = overallyFirstMessage.Id
}
}
fromMessageId = rsmAfterMessage.Id
} else {
iqAnswerSetError(answer, 404)
log.Debugf("MAM: unknown RSM after")
return
}
}
if rsmBefore != 0 {
rsmBeforeMessage, rsmBeforeMessageErr := session.GetMessage(toID, rsmBefore)
if rsmBeforeMessageErr == nil && rsmBeforeMessage != nil {
if int64(rsmBeforeMessage.Date) < startTime.Unix() {
iqAnswerSetError(answer, 400)
log.Debugf("MAM: RSM before out of range")
return
}
if endTime.IsZero() || int64(rsmBeforeMessage.Date) <= endTime.Unix() {
newLastMessage, newLastMessageErr := session.GetPreviousMessage(toID, rsmBeforeMessage.Id)
if newLastMessageErr == nil && newLastMessage != nil {
if lastMessageId == 0 || lastMessageId != newLastMessage.Id {
toEnd = false
lastMessageId = newLastMessage.Id
}
} else {
// nothing?.. not complete, just empty
beyond = true
}
}
} else {
iqAnswerSetError(answer, 404)
log.Debugf("MAM: unknown RSM before")
return
}
}
if !beyond { // yes🗿, twice
messages, complete, err = session.GetMessagesBetween(toID, fromMessageId, lastMessageId, rsmLimit, reverse)
}
}
}
log.Debugf("obtained %v messages", len(messages))
// ty zhe lopnesh, detochka
queryId := ns + " " + query.GetQueryId()
for _, message := range messages {
session.SendDelayedMUCMessage(toID, message, iq.From, queryId)
}
rs := stanza.ResultSet{}
switch ns {
case gateway.NS_MAM2:
answer.Payload = &extensions.MAM2Fin{
ResultSet: &rs,
Complete: complete,
Stable: false,
}
case gateway.NS_MAM1:
answer.Payload = &extensions.MAM1Fin{
ResultSet: &rs,
Complete: complete,
Stable: false,
}
case gateway.NS_MAM0:
answer.Payload = &extensions.MAM0Fin{
ResultSet: &rs,
Complete: complete,
Stable: false,
}
}
if answer.Payload == nil {
log.Error("Unknown MAM version")
return
}
if beyond {
count := 0
rs.Count = &count
log.Debugf("MAM: beyond")
} else {
if len(messages) > 0 {
firstMsgId := messages[0].Id
lastMsgId := messages[len(messages)-1].Id
if fromStart {
if reverse {
overallyFirstMessageId = lastMsgId
} else {
overallyFirstMessageId = firstMsgId
}
}
if toEnd {
if reverse {
overallyLastMessageId = firstMsgId
} else {
overallyLastMessageId = lastMsgId
}
}
}
var firstMsgPositionCount, lastMsgPositionCount int32
// estimate overall count
if overallyFirstMessageId != 0 && overallyLastMessageId != 0 {
firstMsgPosition, firstMsgPositionErr := session.GetChatMessagePosition(toID, overallyFirstMessageId)
if firstMsgPositionErr == nil && firstMsgPosition != nil {
firstMsgPositionCount = firstMsgPosition.Count
}
lastMsgPosition, lastMsgPositionErr := session.GetChatMessagePosition(toID, overallyLastMessageId)
if lastMsgPositionErr == nil && lastMsgPosition != nil {
lastMsgPositionCount = lastMsgPosition.Count
}
log.WithFields(log.Fields{
"overallyFirstMessageId": overallyFirstMessageId,
"overallyLastMessageId": overallyLastMessageId,
"firstMsgPositionCount": firstMsgPositionCount,
"lastMsgPositionCount": lastMsgPositionCount,
}).Debug("RSM count")
if firstMsgPositionCount != 0 && lastMsgPositionCount != 0 {
count := int(firstMsgPositionCount - lastMsgPositionCount + 1)
rs.Count = &count
}
}
if len(messages) > 0 {
firstMsgId := messages[0].Id
lastMsgId := messages[len(messages)-1].Id
if reverse {
firstMsgId, lastMsgId = lastMsgId, firstMsgId
}
rs.First = &stanza.First{
Content: strconv.FormatInt(firstMsgId, 10),
}
if firstMsgPositionCount != 0 {
rsmFirstMsgPosition, rsmFirstMsgPositionErr := session.GetChatMessagePosition(toID, firstMsgId)
if rsmFirstMsgPositionErr == nil && rsmFirstMsgPosition != nil {
index := int(firstMsgPositionCount - rsmFirstMsgPosition.Count)
rs.First.Index = &index
}
}
last := strconv.FormatInt(lastMsgId, 10)
rs.Last = &last
}
}
if log.GetLevel() == log.DebugLevel {
xmlFin, err := xml.Marshal(answer.Payload)
if err == nil {
log.Debug(string(xmlFin))
} else {
log.Debugf("MAM fin: %#v %#v", answer.Payload, rs)
}
}
}
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",
}
case 501:
answer.Error = &stanza.Err{
Type: stanza.ErrorTypeCancel,
Reason: "feature-not-implemented",
}
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 403:
reply.Error.Type = stanza.ErrorTypeAuth
reply.Error.Reason = "forbidden"
case 404:
reply.Error.Type = stanza.ErrorTypeCancel
reply.Error.Reason = "item-not-found"
case 405:
reply.Error.Type = stanza.ErrorTypeCancel
reply.Error.Reason = "not-allowed"
case 407:
reply.Error.Type = stanza.ErrorTypeAuth
reply.Error.Reason = "registration-required"
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, &note)
}
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.OwnChatsKeys() {
chat, _, err := session.GetContactByID(chatId, nil, true)
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)
var chatJid string
if session.Session.MUC && session.IsGroup(chat) {
chatJid = gateway.MUCJID(chat.Id)
} else {
chatJid = gateway.CHATJID(chat.Id, false)
}
gateway.SendPubSubAvatarNotification(component, jid, chatJid, 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 parseMessageId(sId string) (int64, bool) {
if sId[0] == 'e' {
idParts := strings.Split(sId[1:], ":")
if len(idParts) >= 1 {
sId = idParts[0]
}
} else if sId[0] == 'c' {
sId = sId[1:]
}
id, err := strconv.ParseInt(sId, 10, 64)
if err != nil {
log.Warn(errors.Wrap(err, "Failed to parse message ID!"))
return 0, false
}
return id, true
}
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
}
// ToBool returns bool, ok
func ToBool(b string) (bool, bool) {
switch b {
case "0", "false":
return false, true
case "1", "true":
return true, true
}
return false, false
}