package xmpp import ( "bytes" "encoding/base64" "encoding/xml" "github.com/pkg/errors" "io" "strconv" "strings" "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/gateway" log "github.com/sirupsen/logrus" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) const ( TypeVCardTemp byte = iota TypeVCard4 ) const NodeVCard4 string = "urn:xmpp:vcard4" func logPacketType(p stanza.Packet) { log.Warnf("Ignoring packet: %T\n", p) } // HandleIq processes an incoming XMPP iq func HandleIq(s xmpp.Sender, p stanza.Packet) { iq, ok := p.(*stanza.IQ) if !ok { logPacketType(p) return } log.Debugf("%#v", iq) if iq.Type == "get" { _, ok := iq.Payload.(*extensions.IqVcardTemp) if ok { go handleGetVcardIq(s, iq, TypeVCardTemp) return } pubsub, ok := iq.Payload.(*stanza.PubSubGeneric) if ok { if pubsub.Items != nil && pubsub.Items.Node == NodeVCard4 { go handleGetVcardIq(s, iq, TypeVCard4) return } } _, ok = iq.Payload.(*stanza.DiscoInfo) if ok { go handleGetDiscoInfo(s, iq) 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 := splitFrom(msg.From) if !ok { return } gatewayJid := gateway.Jid.Bare() session, ok := sessions[bare] if !ok { if msg.To == gatewayJid { gateway.SendPresence(component, msg.From, gateway.SPType("subscribe")) gateway.SendPresence(component, msg.From, gateway.SPType("subscribed")) } else { log.Error("Message from stranger") } return } toID, ok := toToID(msg.To) if ok { var reply extensions.Reply var fallback extensions.Fallback msg.Get(&reply) msg.Get(&fallback) log.Debugf("reply: %#v", reply) log.Debugf("fallback: %#v", fallback) var replyId int64 var err error text := msg.Body if len(reply.Id) > 0 { 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!")) } text = text[:start] + text[end:] } } session.ProcessOutgoingMessage(toID, text, msg.From, replyId) 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") } } // 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 := splitFrom(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 := splitFrom(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() { go session.ProcessStatusUpdate( status.ID, status.Description, status.XMPP, gateway.SPImmed(false), ) } } }() } } 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 } chat, user, err := session.GetContactByID(toID, nil) if err != nil { log.Error(err) return } var fn, photo, nickname, given, family, tel, info string if chat != nil { fn = chat.Title if chat.Photo != nil { file, path, err := session.OpenPhotoFile(chat.Photo.Small, 32) if err == nil { defer file.Close() buf := new(bytes.Buffer) binval := base64.NewEncoder(base64.StdEncoding, buf) _, err = io.Copy(binval, file) binval.Close() if err == nil { photo = buf.String() } else { log.Errorf("Error calculating base64: %v", path) } } else if path != "" { log.Errorf("Photo does not exist: %v", path) } else { log.Errorf("PHOTO: %#v", err.Error()) } } info = session.GetChatDescription(chat) } if user != nil { nickname = user.Username given = user.FirstName family = user.LastName tel = user.PhoneNumber } answer := stanza.IQ{ Attrs: stanza.Attrs{ From: iq.To, To: iq.From, Id: iq.Id, Type: "result", }, Payload: makeVCardPayload(typ, iq.To, fn, photo, nickname, given, family, tel, info), } log.Debugf("%#v", answer) component, ok := s.(*xmpp.Component) if !ok { log.Error("Not a component") return } _ = gateway.ResumableSend(component, &answer) } func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) { answer, err := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: iq.To, To: iq.From, Id: iq.Id, Lang: "en", }) if err != nil { log.Errorf("Failed to create answer IQ: %v", err) return } disco := answer.DiscoInfo() _, ok := toToID(iq.To) if ok { disco.AddIdentity("", "account", "registered") } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") } 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 splitFrom(from string) (string, string, bool) { fromJid, err := stanza.NewJid(from) if err != nil { log.WithFields(log.Fields{ "from": from, }).Error(errors.Wrap(err, "Invalid from JID!")) return "", "", false } return fromJid.Bare(), fromJid.Resource, true } 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, fn, photo, nickname, given, family, tel, info string) stanza.IQPayload { if typ == TypeVCardTemp { vcard := &extensions.IqVcardTemp{} vcard.Fn.Text = fn if photo != "" { vcard.Photo.Type.Text = "image/jpeg" vcard.Photo.Binval.Text = photo } vcard.Nickname.Text = nickname vcard.N.Given.Text = given vcard.N.Family.Text = family vcard.Tel.Number.Text = tel vcard.Desc.Text = info return vcard } else if typ == TypeVCard4 { nodes := []stanza.Node{} if fn != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "fn"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, Content: fn, }, }, }) } if photo != "" { 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," + photo, }, }, }) } if nickname != "" { 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 family != "" || given != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "n"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "surname"}, Content: family, }, stanza.Node{ XMLName: xml.Name{Local: "given"}, Content: given, }, }, }) } if tel != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "tel"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, Content: "tel:" + tel, }, }, }) } if info != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "note"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, Content: info, }, }, }) } pubsub := &stanza.PubSubGeneric{ Items: &stanza.Items{ Node: NodeVCard4, List: []stanza.Item{ stanza.Item{ Id: id, Any: &stanza.Node{ XMLName: xml.Name{Local: "vcard"}, Attrs: []xml.Attr{ xml.Attr{ Name: xml.Name{Local: "xmlns"}, Value: "urn:ietf:params:xml:ns:vcard-4.0", }, }, Nodes: nodes, }, }, }, }, } return pubsub } return nil }