diff options
Diffstat (limited to 'xmpp')
-rw-r--r-- | xmpp/component.go | 10 | ||||
-rw-r--r-- | xmpp/extensions/extensions.go | 213 | ||||
-rw-r--r-- | xmpp/gateway/gateway.go | 128 | ||||
-rw-r--r-- | xmpp/handlers.go | 450 |
4 files changed, 720 insertions, 81 deletions
diff --git a/xmpp/component.go b/xmpp/component.go index 0f23d50..f0c481d 100644 --- a/xmpp/component.go +++ b/xmpp/component.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/config" "dev.narayana.im/narayana/telegabber/persistence" "dev.narayana.im/narayana/telegabber/telegram" @@ -38,7 +39,7 @@ var sizeRegex = regexp.MustCompile("\\A([0-9]+) ?([KMGTPE]?B?)\\z") // NewComponent starts a new component and wraps it in // a stream manager that you should start yourself -func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig) (*xmpp.StreamManager, *xmpp.Component, error) { +func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig, idsPath string) (*xmpp.StreamManager, *xmpp.Component, error) { var err error gateway.Jid, err = stanza.NewJid(conf.Jid) @@ -53,6 +54,8 @@ func NewComponent(conf config.XMPPConfig, tc config.TelegramConfig) (*xmpp.Strea } } + gateway.IdsDB = badger.IdsDBOpen(idsPath) + tgConf = tc if tc.Content.Quota != "" { @@ -163,6 +166,8 @@ func heartbeat(component *xmpp.Component) { // it would be resolved on the next iteration SaveSessions() } + + gateway.IdsDB.Gc() } } @@ -240,6 +245,9 @@ func Close(component *xmpp.Component) { // save sessions SaveSessions() + // flush the ids database + gateway.IdsDB.Close() + // close stream component.Disconnect() } diff --git a/xmpp/extensions/extensions.go b/xmpp/extensions/extensions.go index fac5e7b..8e2f743 100644 --- a/xmpp/extensions/extensions.go +++ b/xmpp/extensions/extensions.go @@ -2,6 +2,7 @@ package extensions import ( "encoding/xml" + "strconv" "gosrc.io/xmpp/stanza" ) @@ -111,6 +112,107 @@ type IqVcardDesc struct { Text string `xml:",chardata"` } +// Reply is from XEP-0461 +type Reply struct { + XMLName xml.Name `xml:"urn:xmpp:reply:0 reply"` + To string `xml:"to,attr"` + Id string `xml:"id,attr"` +} + +// Fallback is from XEP-0428 +type Fallback struct { + XMLName xml.Name `xml:"urn:xmpp:fallback:0 fallback"` + For string `xml:"for,attr"` + Body []FallbackBody `xml:"urn:xmpp:fallback:0 body"` + Subject []FallbackSubject `xml:"urn:xmpp:fallback:0 subject"` +} + +// FallbackBody is from XEP-0428 +type FallbackBody struct { + XMLName xml.Name `xml:"urn:xmpp:fallback:0 body"` + Start string `xml:"start,attr"` + End string `xml:"end,attr"` +} + +// FallbackSubject is from XEP-0428 +type FallbackSubject struct { + XMLName xml.Name `xml:"urn:xmpp:fallback:0 subject"` + Start string `xml:"start,attr"` + End string `xml:"end,attr"` +} + +// CarbonReceived is from XEP-0280 +type CarbonReceived struct { + XMLName xml.Name `xml:"urn:xmpp:carbons:2 received"` + Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` +} + +// CarbonSent is from XEP-0280 +type CarbonSent struct { + XMLName xml.Name `xml:"urn:xmpp:carbons:2 sent"` + Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` +} + +// ComponentPrivilege is from XEP-0356 +type ComponentPrivilege1 struct { + XMLName xml.Name `xml:"urn:xmpp:privilege:1 privilege"` + Perms []ComponentPerm `xml:"perm"` + Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` +} + +// ComponentPrivilege is from XEP-0356 +type ComponentPrivilege2 struct { + XMLName xml.Name `xml:"urn:xmpp:privilege:2 privilege"` + Perms []ComponentPerm `xml:"perm"` + Forwarded stanza.Forwarded `xml:"urn:xmpp:forward:0 forwarded"` +} + +// ComponentPerm is from XEP-0356 +type ComponentPerm struct { + XMLName xml.Name `xml:"perm"` + Access string `xml:"access,attr"` + Type string `xml:"type,attr"` + Push bool `xml:"push,attr"` +} + +// ClientMessage is a jabber:client NS message compatible with Prosody's XEP-0356 implementation +type ClientMessage struct { + XMLName xml.Name `xml:"jabber:client message"` + stanza.Attrs + + Subject string `xml:"subject,omitempty"` + Body string `xml:"body,omitempty"` + Thread string `xml:"thread,omitempty"` + Error stanza.Err `xml:"error,omitempty"` + Extensions []stanza.MsgExtension `xml:",omitempty"` +} + +// Replace is from XEP-0308 +type Replace struct { + XMLName xml.Name `xml:"urn:xmpp:message-correct:0 replace"` + Id string `xml:"id,attr"` +} + +// QueryRegister is from XEP-0077 +type QueryRegister struct { + XMLName xml.Name `xml:"jabber:iq:register query"` + Instructions string `xml:"instructions"` + Username string `xml:"username"` + Registered *QueryRegisterRegistered `xml:"registered"` + Remove *QueryRegisterRemove `xml:"remove"` + ResultSet *stanza.ResultSet `xml:"set,omitempty"` +} + +// QueryRegisterRegistered is a child element from XEP-0077 +type QueryRegisterRegistered struct { + XMLName xml.Name `xml:"registered"` +} + +// QueryRegisterRemove is a child element from XEP-0077 +type QueryRegisterRemove struct { + XMLName xml.Name `xml:"remove"` +} + // Namespace is a namespace! func (c PresenceNickExtension) Namespace() string { return c.XMLName.Space @@ -131,6 +233,69 @@ func (c IqVcardTemp) GetSet() *stanza.ResultSet { return c.ResultSet } +// Namespace is a namespace! +func (c Reply) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c Fallback) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c CarbonReceived) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c CarbonSent) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c ComponentPrivilege1) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c ComponentPrivilege2) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c Replace) Namespace() string { + return c.XMLName.Space +} + +// Namespace is a namespace! +func (c QueryRegister) Namespace() string { + return c.XMLName.Space +} + +// GetSet getsets! +func (c QueryRegister) GetSet() *stanza.ResultSet { + return c.ResultSet +} + +// Name is a packet name +func (ClientMessage) Name() string { + return "message" +} + +// NewReplyFallback initializes a fallback range +func NewReplyFallback(start uint64, end uint64) Fallback { + return Fallback{ + For: "urn:xmpp:reply:0", + Body: []FallbackBody{ + FallbackBody{ + Start: strconv.FormatUint(start, 10), + End: strconv.FormatUint(end, 10), + }, + }, + } +} + func init() { // presence nick stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{ @@ -149,4 +314,52 @@ func init() { "vcard-temp", "vCard", }, IqVcardTemp{}) + + // reply + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:reply:0", + "reply", + }, Reply{}) + + // fallback + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:fallback:0", + "fallback", + }, Fallback{}) + + // carbon received + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:carbons:2", + "received", + }, CarbonReceived{}) + + // carbon sent + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:carbons:2", + "sent", + }, CarbonSent{}) + + // component privilege v1 + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:privilege:1", + "privilege", + }, ComponentPrivilege1{}) + + // component privilege v2 + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:privilege:2", + "privilege", + }, ComponentPrivilege2{}) + + // message edit + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{ + "urn:xmpp:message-correct:0", + "replace", + }, Replace{}) + + // register query + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{ + "jabber:iq:register", + "query", + }, QueryRegister{}) } diff --git a/xmpp/gateway/gateway.go b/xmpp/gateway/gateway.go index d4620e6..dfe2ebf 100644 --- a/xmpp/gateway/gateway.go +++ b/xmpp/gateway/gateway.go @@ -2,9 +2,11 @@ package gateway import ( "encoding/xml" + "github.com/pkg/errors" "strings" "sync" + "dev.narayana.im/narayana/telegabber/badger" "dev.narayana.im/narayana/telegabber/xmpp/extensions" log "github.com/sirupsen/logrus" @@ -13,6 +15,13 @@ import ( "gosrc.io/xmpp/stanza" ) +type Reply struct { + Author string + Id string + Start uint64 + End uint64 +} + const NSNick string = "http://jabber.org/protocol/nick" // Queue stores presences to send later @@ -22,16 +31,51 @@ var QueueLock = sync.Mutex{} // Jid stores the component's JID object var Jid *stanza.Jid +// IdsDB provides a disk-backed bidirectional dictionary of Telegram and XMPP ids +var IdsDB badger.IdsDB + // DirtySessions denotes that some Telegram session configurations // were changed and need to be re-flushed to the YamlDB var DirtySessions = false +// MessageOutgoingPermissionVersion contains a XEP-0356 version to fake outgoing messages by foreign JIDs +var MessageOutgoingPermissionVersion = 0 + // SendMessage creates and sends a message stanza -func SendMessage(to string, from string, body string, component *xmpp.Component) { +func SendMessage(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, isCarbon bool) { + sendMessageWrapper(to, from, body, id, component, reply, "", isCarbon) +} + +// SendServiceMessage creates and sends a simple message stanza from transport +func SendServiceMessage(to string, body string, component *xmpp.Component) { + sendMessageWrapper(to, "", body, "", component, nil, "", false) +} + +// SendTextMessage creates and sends a simple message stanza +func SendTextMessage(to string, from string, body string, component *xmpp.Component) { + sendMessageWrapper(to, from, body, "", component, nil, "", false) +} + +// SendMessageWithOOB creates and sends a message stanza with OOB URL +func SendMessageWithOOB(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { + sendMessageWrapper(to, from, body, id, component, reply, oob, isCarbon) +} + +func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, oob string, isCarbon bool) { + toJid, err := stanza.NewJid(to) + if err != nil { + log.WithFields(log.Fields{ + "to": to, + }).Error(errors.Wrap(err, "Invalid to JID!")) + return + } + bareTo := toJid.Bare() + componentJid := Jid.Full() var logFrom string var messageFrom string + var messageTo string if from == "" { logFrom = componentJid messageFrom = componentJid @@ -39,6 +83,12 @@ func SendMessage(to string, from string, body string, component *xmpp.Component) logFrom = from messageFrom = from + "@" + componentJid } + if isCarbon { + messageTo = messageFrom + messageFrom = bareTo + "/" + Jid.Resource + } else { + messageTo = to + } log.WithFields(log.Fields{ "from": logFrom, @@ -48,13 +98,67 @@ func SendMessage(to string, from string, body string, component *xmpp.Component) message := stanza.Message{ Attrs: stanza.Attrs{ From: messageFrom, - To: to, + To: messageTo, Type: "chat", + Id: id, }, Body: body, } - sendMessage(&message, component) + if oob != "" { + message.Extensions = append(message.Extensions, stanza.OOB{ + URL: oob, + }) + } + if reply != nil { + message.Extensions = append(message.Extensions, extensions.Reply{ + To: reply.Author, + Id: reply.Id, + }) + if reply.End > 0 { + message.Extensions = append(message.Extensions, extensions.NewReplyFallback(reply.Start, reply.End)) + } + } + if !isCarbon && toJid.Resource != "" { + message.Extensions = append(message.Extensions, stanza.HintNoCopy{}) + } + + if isCarbon { + carbonMessage := extensions.ClientMessage{ + Attrs: stanza.Attrs{ + From: bareTo, + To: to, + Type: "chat", + }, + } + carbonMessage.Extensions = append(carbonMessage.Extensions, extensions.CarbonSent{ + Forwarded: stanza.Forwarded{ + Stanza: extensions.ClientMessage(message), + }, + }) + privilegeMessage := stanza.Message{ + Attrs: stanza.Attrs{ + From: Jid.Bare(), + To: toJid.Domain, + }, + } + if MessageOutgoingPermissionVersion == 2 { + privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege2{ + Forwarded: stanza.Forwarded{ + Stanza: carbonMessage, + }, + }) + } else { + privilegeMessage.Extensions = append(privilegeMessage.Extensions, extensions.ComponentPrivilege1{ + Forwarded: stanza.Forwarded{ + Stanza: carbonMessage, + }, + }) + } + sendMessage(&privilegeMessage, component) + } else { + sendMessage(&message, component) + } } // SetNickname sets a new nickname for a contact @@ -255,3 +359,21 @@ func ResumableSend(component *xmpp.Component, packet stanza.Packet) error { } return err } + +// SubscribeToTransport ensures a two-way subscription to the transport +func SubscribeToTransport(component *xmpp.Component, jid string) { + SendPresence(component, jid, SPType("subscribe")) + SendPresence(component, jid, SPType("subscribed")) +} + +// SplitJID tokenizes a JID string to bare JID and resource +func SplitJID(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 +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index 0de33b8..96a30c4 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -4,16 +4,19 @@ import ( "bytes" "encoding/base64" "encoding/xml" + "fmt" "github.com/pkg/errors" "io" "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" log "github.com/sirupsen/logrus" + "github.com/soheilhy/args" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) @@ -66,6 +69,17 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) { go handleGetDisco(discoTypeItems, s, iq) return } + _, ok = iq.Payload.(*extensions.QueryRegister) + if ok { + go handleGetQueryRegister(s, iq) + return + } + } else if iq.Type == "set" { + query, ok := iq.Payload.(*extensions.QueryRegister) + if ok { + go handleSetQueryRegister(s, iq, query) + return + } } } @@ -90,7 +104,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { }).Warn("Message") log.Debugf("%#v", msg) - bare, resource, ok := splitFrom(msg.From) + bare, resource, ok := gateway.SplitJID(msg.From) if !ok { return } @@ -100,30 +114,164 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) { 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")) + gateway.SubscribeToTransport(component, msg.From) } else { log.Error("Message from stranger") - return } + return } toID, ok := toToID(msg.To) if ok { - session.ProcessOutgoingMessage(toID, msg.Body, msg.From) + 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), "<ERROR: Chat mismatch>", component) + return + } + replaceId = msgId + log.Debugf("replace tg: %#v %#v", chatId, msgId) + } else { + gateway.SendTextMessage(msg.From, strconv.FormatInt(toID, 10), "<ERROR: Could not find matching message to edit>", 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.AddToOutbox(replace.Id, resource) + } else { + err = gateway.IdsDB.Set(session.Session.Login, bare, toID, tgMessageId, msg.Id) + if err != nil { + 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.SendMessage(msg.From, "", response, component) + 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 + } + } + } + + 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 @@ -168,7 +316,7 @@ func handleSubscription(s xmpp.Sender, p stanza.Presence) { if !ok { return } - bare, _, ok := splitFrom(p.From) + bare, _, ok := gateway.SplitJID(p.From) if !ok { return } @@ -199,7 +347,7 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { log.Debugf("%#v", p) // create session - bare, resource, ok := splitFrom(p.From) + bare, resource, ok := gateway.SplitJID(p.From) if !ok { return } @@ -229,13 +377,21 @@ func handlePresence(s xmpp.Sender, p stanza.Presence) { 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, - status.Description, - status.XMPP, - gateway.SPImmed(false), + description, + show, + newArgs..., ) } + session.UpdateChatNicknames() } }() } @@ -265,45 +421,12 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { log.Error("Invalid IQ to") return } - chat, user, err := session.GetContactByID(toID, nil) + info, err := session.GetVcardInfo(toID) 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, @@ -311,7 +434,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { Id: iq.Id, Type: "result", }, - Payload: makeVCardPayload(typ, iq.To, fn, photo, nickname, given, family, tel, info), + Payload: makeVCardPayload(typ, iq.To, info, session), } log.Debugf("%#v", answer) @@ -340,12 +463,15 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { if dt == discoTypeInfo { disco := answer.DiscoInfo() toID, toOk := toToID(iq.To) - if !toOk { + if toOk { + disco.AddIdentity("", "account", "registered") + } else { disco.AddIdentity("Telegram Gateway", "gateway", "telegram") + disco.AddFeatures("jabber:iq:register") } var isMuc bool - bare, _, fromOk := splitFrom(iq.From) + bare, _, fromOk := gateway.SplitJID(iq.From) if fromOk { session, sessionOk := sessions[bare] if sessionOk && session.Session.MUC { @@ -399,7 +525,7 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { _, ok := toToID(iq.To) if !ok { - bare, _, ok := splitFrom(iq.From) + bare, _, ok := gateway.SplitJID(iq.From) if ok { // raw access, no need to create a new instance if not connected session, ok := sessions[bare] @@ -428,15 +554,163 @@ func handleGetDisco(dt discoType, s xmpp.Sender, iq *stanza.IQ) { _ = gateway.ResumableSend(component, answer) } -func splitFrom(from string) (string, string, bool) { - fromJid, err := stanza.NewJid(from) +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.WithFields(log.Fields{ - "from": from, - }).Error(errors.Wrap(err, "Invalid from JID!")) - return "", "", false + 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 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", + } } - return fromJid.Bare(), fromJid.Resource, true } func toToID(to string) (int64, bool) { @@ -454,47 +728,69 @@ func toToID(to string) (int64, bool) { return toID, true } -func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, info string) stanza.IQPayload { +func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload { + var base64Photo string + if info.Photo != nil { + file, path, err := session.ForceOpenFile(info.Photo, 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 { + base64Photo = 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()) + } + } + if typ == TypeVCardTemp { vcard := &extensions.IqVcardTemp{} - vcard.Fn.Text = fn - if photo != "" { + vcard.Fn.Text = info.Fn + if base64Photo != "" { vcard.Photo.Type.Text = "image/jpeg" - vcard.Photo.Binval.Text = photo + vcard.Photo.Binval.Text = base64Photo } - vcard.Nickname.Text = nickname - vcard.N.Given.Text = given - vcard.N.Family.Text = family - vcard.Tel.Number.Text = tel - vcard.Desc.Text = info + 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 fn != "" { + if info.Fn != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "fn"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: fn, + Content: info.Fn, }, }, }) } - if photo != "" { + 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," + photo, + Content: "data:image/jpeg;base64," + base64Photo, }, }, }) } - if nickname != "" { + for _, nickname := range info.Nicknames { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "nickname"}, Nodes: []stanza.Node{ @@ -513,39 +809,39 @@ func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, inf }, }) } - if family != "" || given != "" { + 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: family, + Content: info.Family, }, stanza.Node{ XMLName: xml.Name{Local: "given"}, - Content: given, + Content: info.Given, }, }, }) } - if tel != "" { + 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:" + tel, + Content: "tel:" + info.Tel, }, }, }) } - if info != "" { + 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, + Content: info.Info, }, }, }) |