aboutsummaryrefslogtreecommitdiff
path: root/xmpp/handlers.go
diff options
context:
space:
mode:
authorBohdan Horbeshko <bodqhrohro@gmail.com>2023-09-17 06:16:09 +0300
committerBohdan Horbeshko <bodqhrohro@gmail.com>2023-09-17 06:16:09 +0300
commit9dbd487dae9b5a74981873722be685d3706ab772 (patch)
treef5d5ab8fc1718a58cd5630aa6365f624a6fdbe98 /xmpp/handlers.go
parent7eaf28ad7c4d2bdf5aa6313503d751de90a6811c (diff)
parent282a6fc21b9626ab1bdc9c5a78162d90b7d28aa2 (diff)
Merge branch 'master' into muc
Diffstat (limited to 'xmpp/handlers.go')
-rw-r--r--xmpp/handlers.go450
1 files changed, 373 insertions, 77 deletions
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,
},
},
})