package xmpp import ( "encoding/xml" "fmt" "github.com/pkg/errors" "sort" "strconv" "strings" "sync" "time" "dev.narayana.im/narayana/telegabber/e2ee" "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 != "" || hasOMEMOPayload(msg)) { 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 // OMEMO decrypt hook - personal-chat pseudo-JIDs only (true // MUC/XEP-0045 groupchat OMEMO is out of scope entirely, not // deferred - see the plan doc). A successful decrypt is itself // the auto-upgrade signal for this chat (enablement trigger 2); // the account-wide config toggle (trigger 1) is a separate, // not-yet-wired-up piece (M4). if !isGroupchat { if backend, backendOk := gateway.E2EE.Backend(); backendOk { env, hasEnv, decErr := decodeOMEMOEnvelope(msg) if decErr != nil { log.Error(errors.Wrap(decErr, "Failed to decode OMEMO envelope")) return } if hasEnv { owner := e2ee.OwnedPeer(session.Session.Login, toJid.Bare()) plaintext, decErr := backend.Decrypt(e2ee.PeerID(bare), owner, env) if decErr != nil { log.Error(errors.Wrap(decErr, "Failed to decrypt OMEMO message")) return } text = string(plaintext) if enabled, _ := backend.Enabled(owner); !enabled { if err := backend.SetEnabled(owner, true); err != nil { log.Error(errors.Wrap(err, "Failed to mark chat as OMEMO-active")) } } } } } if text == "" { // Either e2ee is disabled and this was an OMEMO-only // (body-less) stanza with nothing decryptable, or some // other body-less case the relaxed gate above now lets // through - nothing sensible to forward to Telegram. return } 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), "", 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), "", 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), component, gateway.SMBody(text), gateway.SMIsGroupchat(isGroupchat)) } 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, ¬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.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 }