aboutsummaryrefslogtreecommitdiff
path: root/xmpp
diff options
context:
space:
mode:
Diffstat (limited to 'xmpp')
-rw-r--r--xmpp/component.go10
-rw-r--r--xmpp/extensions/extensions.go213
-rw-r--r--xmpp/gateway/gateway.go128
-rw-r--r--xmpp/handlers.go450
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,
},
},
})