package xmpp import ( "encoding/xml" "fmt" "github.com/pkg/errors" "sort" "strconv" "strings" "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/telegram" "dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/gateway" "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/soheilhy/args" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) const ( TypeVCardTemp byte = iota TypeVCard4 ) func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) } // HandleIq processes an incoming XMPP iq func HandleIq(s xmpp.Sender, p stanza.Packet) { iq, ok := p.(*stanza.IQ) if !ok { logPacketType(p) return } log.Debugf("%#v", iq) if iq.Type == stanza.IQTypeGet { _, ok := iq.Payload.(*extensions.IqVcardTemp) if ok { go handleGetVcardIq(s, iq, TypeVCardTemp) return } pubsub, ok := iq.Payload.(*stanza.PubSubGeneric) if ok && pubsub.Items != nil { if pubsub.Items.Node == gateway.NodeVCard4 { go handleGetVcardIq(s, iq, TypeVCard4) return } if pubsub.Items.Node == gateway.NodeAvatarData { go handleGetAvatarDataIq(s, iq, pubsub) return } } discoInfo, ok := iq.Payload.(*stanza.DiscoInfo) if ok { go handleGetDiscoInfo(s, iq, discoInfo) return } discoItems, ok := iq.Payload.(*stanza.DiscoItems) if ok { go handleGetDiscoItems(s, iq, discoItems) return } _, ok = iq.Payload.(*extensions.QueryRegister) if ok { go handleGetQueryRegister(s, iq) return } } else if iq.Type == stanza.IQTypeSet { query, ok := iq.Payload.(*extensions.QueryRegister) if ok { go handleSetQueryRegister(s, iq, query) return } command, ok := iq.Payload.(*stanza.Command) if ok { go handleSetQueryCommand(s, iq, command) return } } 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 := toToID(msg.To) if ok { var reply extensions.Reply var fallback extensions.Fallback var replace extensions.Replace msg.Get(&reply) msg.Get(&fallback) msg.Get(&replace) log.Debugf("reply: %#v", reply) log.Debugf("fallback: %#v", fallback) log.Debugf("replace: %#v", replace) var replyId int64 var err error text := msg.Body if len(reply.Id) > 0 { chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, reply.Id) if err == nil { if chatId != toID { log.Warnf("Chat mismatch: %v ≠ %v", chatId, toID) } else { replyId = msgId log.Debugf("replace tg: %#v %#v", chatId, msgId) } } else { id := reply.Id if id[0] == 'e' { id = id[1:] } replyId, err = strconv.ParseInt(id, 10, 64) if err != nil { log.Warn(errors.Wrap(err, "Failed to parse message ID!")) } } if replyId != 0 && fallback.For == "urn:xmpp:reply:0" && len(fallback.Body) > 0 { body := fallback.Body[0] var start, end int64 start, err = strconv.ParseInt(body.Start, 10, 64) if err != nil { log.WithFields(log.Fields{ "start": body.Start, }).Warn(errors.Wrap(err, "Failed to parse fallback start!")) } end, err = strconv.ParseInt(body.End, 10, 64) if err != nil { log.WithFields(log.Fields{ "end": body.End, }).Warn(errors.Wrap(err, "Failed to parse fallback end!")) } fullRunes := []rune(text) cutRunes := make([]rune, 0, len(text)-int(end-start)) cutRunes = append(cutRunes, fullRunes[:start]...) cutRunes = append(cutRunes, fullRunes[end:]...) text = string(cutRunes) } } var replaceId int64 if replace.Id != "" { chatId, msgId, err := gateway.IdsDB.GetByXmppId(session.Session.Login, bare, replace.Id) if err == nil { if chatId != toID { gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "", component) return } replaceId = msgId log.Debugf("replace tg: %#v %#v", chatId, msgId) } else { gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "", component) return } } session.SendMessageLock.Lock() defer session.SendMessageLock.Unlock() tgMessageId := session.ProcessOutgoingMessage(toID, text, msg.From, replyId, replaceId) if tgMessageId != 0 { if replaceId != 0 { // not needed (is it persistent among clients though?) /* err = gateway.IdsDB.ReplaceIdPair(session.Session.Login, bare, replace.Id, msg.Id, tgMessageId) if err != nil { log.Errorf("Failed to replace id %v with %v %v", replace.Id, msg.Id, tgMessageId) } */ session.AddToEditOutbox(replace.Id, resource) } else { err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) if err == nil { // session.AddToOutbox(msg.Id, resource) session.UpdateLastChatMessageId(toID, msg.Id) } else { log.Errorf("Failed to save ids %v/%v %v", toID, tgMessageId, msg.Id) } } } else { /* // if a message failed to edit on Telegram side, match new XMPP ID with old Telegram ID anyway if replaceId != 0 { err = gateway.IdsDB.ReplaceXmppId(session.Session.Login, bare, replace.Id, msg.Id) if err != nil { log.Errorf("Failed to replace id %v with %v", replace.Id, msg.Id) } } */ } return } else { toJid, err := stanza.NewJid(msg.To) if err == nil && toJid.Bare() == gatewayJid && (strings.HasPrefix(msg.Body, "/") || strings.HasPrefix(msg.Body, "!")) { response, _ := session.ProcessTransportCommand(msg.Body, resource) if response != "" { gateway.SendServiceMessage(msg.From, response, component) } return } } log.Warn("Unknown purpose of the message, skipping") } if msg.Body == "" { var privilege1 extensions.ComponentPrivilege1 if ok := msg.Get(&privilege1); ok { log.Debugf("privilege1: %#v", privilege1) } for _, perm := range privilege1.Perms { if perm.Access == "message" && perm.Type == "outgoing" { gateway.MessageOutgoingPermissionVersion = 1 } } var privilege2 extensions.ComponentPrivilege2 if ok := msg.Get(&privilege2); ok { log.Debugf("privilege2: %#v", privilege2) } for _, perm := range privilege2.Perms { if perm.Access == "message" && perm.Type == "outgoing" { gateway.MessageOutgoingPermissionVersion = 2 } } var displayed stanza.MarkDisplayed msg.Get(&displayed) if displayed.ID != "" { log.Debugf("displayed: %#v", displayed) bare, _, ok := gateway.SplitJID(msg.From) if !ok { return } session, ok := sessions[bare] if !ok { return } toID, ok := toToID(msg.To) if !ok { return } msgId, err := strconv.ParseInt(displayed.ID, 10, 64) if err == nil { session.MarkAsRead(toID, msgId) } return } } if msg.Type == "error" { log.Errorf("MESSAGE ERROR: %#v", p) if msg.XMLName.Space == "jabber:component:accept" && msg.Error.Code == 401 { suffix := "@" + msg.From for bare := range sessions { if strings.HasSuffix(bare, suffix) { gateway.SendServiceMessage(bare, "Your server \""+msg.From+"\" does not allow to send carbons", component) } } } } } // HandlePresence processes an incoming XMPP presence func HandlePresence(s xmpp.Sender, p stanza.Packet) { prs, ok := p.(stanza.Presence) if !ok { logPacketType(p) return } if prs.Type == "subscribe" { handleSubscription(s, prs) } if prs.To == gateway.Jid.Bare() { handlePresence(s, prs) } } func handleSubscription(s xmpp.Sender, p stanza.Presence) { log.WithFields(log.Fields{ "from": p.From, "to": p.To, }).Warn("Subscription request") log.Debugf("%#v", p) reply := stanza.Presence{Attrs: stanza.Attrs{ From: p.To, To: p.From, Id: p.Id, Type: "subscribed", }} component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } _ = gateway.ResumableSend(component, reply) toID, ok := toToID(p.To) if !ok { return } bare, _, ok := gateway.SplitJID(p.From) if !ok { return } session, ok := getTelegramInstance(bare, &persistence.Session{}, component) if !ok { return } go session.ProcessStatusUpdate(toID, "", "", gateway.SPImmed(false)) } func handlePresence(s xmpp.Sender, p stanza.Presence) { presenceType := p.Type if presenceType == "" { presenceType = "online" } component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } log.WithFields(log.Fields{ "type": presenceType, "from": p.From, "to": p.To, }).Warn("Presence") log.Debugf("%#v", p) // create session bare, resource, ok := gateway.SplitJID(p.From) if !ok { return } session, ok := getTelegramInstance(bare, &persistence.Session{}, component) if !ok { return } switch p.Type { // destroy session case "unsubscribed", "unsubscribe": if session.Disconnect(resource, false) { sessionLock.Lock() delete(sessions, bare) sessionLock.Unlock() } // go offline case "unavailable", "error": session.Disconnect(resource, false) // go online case "probe", "", "online", "subscribe": // due to the weird implementation of go-tdlib wrapper, it won't // return the client instance until successful authorization go func() { err := session.Connect(resource) if err != nil { log.Error(errors.Wrap(err, "TDlib connection failure")) } else { for status := range session.StatusesRange() { show, description, typ := status.Destruct() newArgs := []args.V{ gateway.SPImmed(false), } if typ != "" { newArgs = append(newArgs, gateway.SPType(typ)) } go session.ProcessStatusUpdate( status.ID, description, show, newArgs..., ) } probeClientFeatures(p.From, component) session.UpdateChatNicknames() } }() } } func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { log.WithFields(log.Fields{ "from": iq.From, "to": iq.To, }).Warn("VCard request") fromJid, err := stanza.NewJid(iq.From) if err != nil { log.Error("Invalid from JID!") return } session, ok := sessions[fromJid.Bare()] if !ok { log.Error("IQ from stranger") return } toParts := strings.Split(iq.To, "@") toID, err := strconv.ParseInt(toParts[0], 10, 64) if err != nil { log.Error("Invalid IQ to") return } info, err := session.GetVcardInfo(toID) if err != nil { log.Error(err) return } answer := stanza.IQ{ Attrs: stanza.Attrs{ From: iq.To, To: iq.From, Id: iq.Id, Type: "result", }, Payload: makeVCardPayload(typ, iq.To, info, session), } log.Debugf("%#v", answer) component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } _ = gateway.ResumableSend(component, &answer) } func handleGetAvatarDataIq(s xmpp.Sender, iq *stanza.IQ, pubsub *stanza.PubSubGeneric) { fromJid, err := stanza.NewJid(iq.From) if err != nil { log.Errorf("Invalid from JID %v", iq.From) return } chatId, ok := toToID(iq.To) if !ok { log.Errorf("Invalid chat id in To JID %v", iq.To) return } session, ok := sessions[fromJid.Bare()] if !ok { log.Errorf("IQ from stranger %v", iq.From) return } var id string if len(pubsub.Items.List) > 0 { id = pubsub.Items.List[0].Id } log.Infof("Avatar id %v for chat %v", id, iq.To) pubsubAnswer := stanza.PubSubGeneric{ Items: &stanza.Items{ Node: gateway.NodeAvatarData, }, } answer := stanza.IQ{ Attrs: stanza.Attrs{ From: iq.To, To: iq.From, Id: iq.Id, Type: "result", }, Payload: &pubsubAnswer, } component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } defer gateway.ResumableSend(component, &answer) hashedAvatar, ok := session.AvatarHashes[chatId] if !ok { log.Info("Could not find avatar in cache, fetching immediately") chat, _, err := session.GetContactByID(chatId, nil) if err != nil || chat == nil || chat.Photo == nil { return } file := chat.Photo.Small sha1 := session.GetPhotoSha1(file, chatId) hashedAvatar = &telegram.HashedAvatar{ Hash: sha1, File: file.Id, } session.AvatarHashesLock.Lock() session.AvatarHashes[chatId] = hashedAvatar session.AvatarHashesLock.Unlock() } if id != "" && hashedAvatar.Hash != id { log.Infof("Cache contains %v hash for chat %v, but %v was requested; aborting", hashedAvatar.Hash, iq.To, id) return } if hashedAvatar.File == 0 { log.Infof("Avatar for chat %v is explicitly missing", iq.To) return } file, err := session.GetFile(hashedAvatar.File) if err != nil { log.WithFields(log.Fields{ "chatId": chatId, }).Error(errors.Wrap(err, "Cannot get avatar file")) return } dataString := session.GetPhotoBase64(file) if dataString == "" { log.Errorf("Error reading avatar file for chat %v", iq.To) return } pubsubAnswer.Items.List = append(pubsubAnswer.Items.List, stanza.Item{ Id: hashedAvatar.Hash, Any: &stanza.Node{ XMLName: xml.Name{Local: "data", Space: gateway.NodeAvatarData}, Content: dataString, }, }) log.WithFields(log.Fields{ "length": len(dataString), }).Debugf("%#v", answer) } func getTelegramChatType(from string, to string) (telegram.ChatType, error) { toId, ok := toToID(to) if ok { bare, _, ok := gateway.SplitJID(from) if ok { session, ok := sessions[bare] if ok { return session.GetChatType(toId) } } } return telegram.ChatTypeUnknown, errors.New("Unknown chat type") } func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, To: iq.From, Id: iq.Id, Lang: "en", }) if err != nil { log.Errorf("Failed to create answer IQ: %v", err) return } disco := answer.DiscoInfo() _, ok := toToID(iq.To) if di.Node == "" { if ok { disco.AddIdentity("", "account", "registered") disco.AddFeatures(stanza.NSMsgChatMarkers) disco.AddFeatures(stanza.NSMsgReceipts) } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddFeatures("jabber:iq:register") } disco.AddFeatures(gateway.NSCommand) } else { chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) var cmdType telegram.CommandType if ok { 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 } answer.Payload = di di.AddIdentity(telegram.CommandToHelpString(name, command), "automation", "command-node") di.AddFeatures(gateway.NSCommand, "jabber:x:data") break } } } answer.Payload = disco log.Debugf("%#v", answer) component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } _ = gateway.ResumableSend(component, answer) } func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoItems) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, To: iq.From, Id: iq.Id, Lang: "en", }) if err != nil { log.Errorf("Failed to create answer IQ: %v", err) return } log.Debugf("discoItems: %#v", di) _, ok := toToID(iq.To) if di.Node == gateway.NSCommand { answer.Payload = di chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To) var cmdType telegram.CommandType if ok { cmdType = telegram.CommandTypeChat } else { cmdType = telegram.CommandTypeTransport } commands := telegram.GetCommands(cmdType) for _, name := range telegram.SortedCommandKeys(commands) { command := commands[name] if chatTypeErr == nil && !telegram.IsCommandForChatType(command, chatType) { continue } di.AddItem(iq.To, name, telegram.CommandToHelpString(name, command)) } } else { answer.Payload = answer.DiscoItems() } component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } _ = gateway.ResumableSend(component, answer) } func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) { component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, To: iq.From, Id: iq.Id, Lang: "en", }) if err != nil { log.Errorf("Failed to create answer IQ: %v", err) return } var login string bare, _, ok := gateway.SplitJID(iq.From) if ok { session, ok := sessions[bare] if ok { login = session.Session.Login } } var query stanza.IQPayload if login == "" { query = extensions.QueryRegister{ Instructions: fmt.Sprintf("Authorization in Telegram is a multi-step process, so please accept %v to your contacts and follow further instructions (provide the authentication code there, etc.).\nFor now, please provide your login.", iq.To), } } else { query = extensions.QueryRegister{ Instructions: "Already logged in", Username: login, Registered: &extensions.QueryRegisterRegistered{}, } } answer.Payload = query log.Debugf("%#v", query) _ = gateway.ResumableSend(component, answer) if login == "" { gateway.SubscribeToTransport(component, iq.From) } } func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) { component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, To: iq.From, Id: iq.Id, Lang: "en", }) if err != nil { log.Errorf("Failed to create answer IQ: %v", err) return } defer gateway.ResumableSend(component, answer) if query.Remove != nil { iqAnswerSetError(answer, query, 405) return } var login string var session *telegram.Client bare, resource, ok := gateway.SplitJID(iq.From) if ok { session, ok = sessions[bare] if ok { login = session.Session.Login } } if login == "" { if !ok { session, ok = getTelegramInstance(bare, &persistence.Session{}, component) if !ok { iqAnswerSetError(answer, query, 500) return } } err := session.TryLogin(resource, query.Username) if err != nil { if err.Error() == telegram.TelegramAuthDone { iqAnswerSetError(answer, query, 406) } else { iqAnswerSetError(answer, query, 500) } return } err = session.SetPhoneNumber(query.Username) if err != nil { iqAnswerSetError(answer, query, 500) return } // everything okay, the response should be empty with no payload/error at this point gateway.SubscribeToTransport(component, iq.From) } else { iqAnswerSetError(answer, query, 406) } } func handleSetQueryCommand(s xmpp.Sender, iq *stanza.IQ, command *stanza.Command) { component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, To: iq.From, Id: iq.Id, Lang: "en", }) if err != nil { log.Errorf("Failed to create answer IQ: %v", err) return } defer gateway.ResumableSend(component, answer) log.Debugf("command: %#v", command) bare, resource, ok := gateway.SplitJID(iq.From) if !ok { return } toId, toOk := toToID(iq.To) var cmdString string var cmdType telegram.CommandType var form *stanza.Form for _, ce := range command.CommandElements { fo, formOk := ce.(*stanza.Form) if formOk { form = fo break } } if toOk { cmdType = telegram.CommandTypeChat } else { cmdType = telegram.CommandTypeTransport } if form != nil { if command.Node == "config" { session, ok := sessions[bare] if ok { var infoStrings []string var warnString, errString string for _, field := range form.Fields { if len(field.ValuesList) > 0 { fieldValue := field.ValuesList[0] if gateway.MessageOutgoingPermissionVersion == 0 && field.Var == "carbons" && fieldValue == "true" { warnString = "The server did not allow to enable carbons" continue } // 10. In accordance with Section 3.2.2.1 of XML Schema Part 2: Datatypes, the allowable // lexical representations for the xs:boolean datatype are the strings "0" and "false" // for the concept 'false' and the strings "1" and "true" for the concept 'true'; // implementations MUST support both styles of lexical representation. if persistence.PropertyType(field.Var) == persistence.PropertyTypeBool { if fieldValue == "0" { fieldValue = "false" } if fieldValue == "1" { fieldValue = "true" } } oldValue, err := session.Session.Get(field.Var) if err != nil || oldValue != fieldValue { value, err := session.Session.Set(field.Var, fieldValue) if err != nil { errString = fmt.Sprintf("Error for field %v: %v, aborting", field.Var, err.Error()) break } 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 { // 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 { cmdString = "/" + command.Node } } else if command.Action == stanza.CommandActionCancel { answer.Payload = &stanza.Command{ SessionId: command.Node, Node: command.Node, Status: stanza.CommandStatusCancelled, } } } if cmdString != "" { session, ok := sessions[bare] if !ok { return } 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", answer.Payload) } func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) { answer.Type = stanza.IQTypeError answer.Payload = *payload switch code { case 400: answer.Error = &stanza.Err{ Code: code, Type: stanza.ErrorTypeModify, Reason: "bad-request", } case 405: answer.Error = &stanza.Err{ Code: code, Type: stanza.ErrorTypeCancel, Reason: "not-allowed", Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport", } case 406: answer.Error = &stanza.Err{ Code: code, Type: stanza.ErrorTypeModify, Reason: "not-acceptable", Text: "Phone number already provided, chat with the transport for further instruction", } case 500: answer.Error = &stanza.Err{ Code: code, Type: stanza.ErrorTypeWait, Reason: "internal-server-error", } default: log.Error("Unknown error code, falling back with empty reason") answer.Error = &stanza.Err{ Code: code, Type: stanza.ErrorTypeCancel, Reason: "undefined-condition", } } } func probeClientFeatures(jid string, component *xmpp.Component) { id, err := uuid.NewRandom() if err != nil { log.Error("Could not generate ID for a client features probe") return } probe := stanza.IQ{ Attrs: stanza.Attrs{ From: gateway.Jid.Bare(), To: jid, Id: id.String(), Type: stanza.IQTypeGet, }, Payload: &stanza.DiscoInfo{}, } log.Debugf("%#v", probe) gateway.ResumableSend(component, &probe) } func handleClientFeatures(s xmpp.Sender, iq *stanza.IQ, discoInfo *stanza.DiscoInfo) { fromJid, err := stanza.NewJid(iq.From) if err != nil { log.Error("Invalid from JID!") return } bareFrom := fromJid.Bare() session, ok := sessions[bareFrom] if !ok { log.Errorf("Got client features for unknown JID %v", bareFrom) return } var features []string var avatarNotify bool for _, feature := range discoInfo.Features { features = append(features, feature.Var) if feature.Var == gateway.NodeAvatarMetadataNotify { avatarNotify = true } } session.XmppClientFeaturesLock.Lock() session.XmppClientFeatures[fromJid.Resource] = &features session.XmppClientFeaturesLock.Unlock() log.Debugf("Features for %v: %#v", iq.From, features) if avatarNotify { go sendPubSubAvatarNotifications(s, iq.From, session) } } func sendPubSubAvatarNotifications(s xmpp.Sender, jid string, session *telegram.Client) { component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } for _, chatId := range session.ChatsKeys() { chat, _, err := session.GetContactByID(chatId, nil) if err != nil || chat == nil { continue } if chat.Photo == nil { session.SetEmptyAvatarHash(chatId) continue } sha1, size := session.GetPhotoSha1AndSize(chat.Photo.Small, chat.Id) gateway.SendPubSubAvatarNotification(component, jid, chat.Id, sha1, size) } } func toToID(to string) (int64, bool) { toParts := strings.Split(to, "@") if len(toParts) < 2 { return 0, false } toID, err := strconv.ParseInt(toParts[0], 10, 64) if err != nil { log.WithFields(log.Fields{ "to": to, }).Error(errors.Wrap(err, "Invalid to JID!")) return 0, false } return toID, true } func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload { var base64Photo string if info.Photo != nil { 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 }