aboutsummaryrefslogblamecommitdiff
path: root/xmpp/handlers.go
blob: 51fd831f4187ed68b63fcfa6697677a0be2892df (plain) (tree)
1
2
3
4
5
6
7
8
9
10


            

                         
                      
                               
            

                 
 
                                                         
                                                      
                                                             
                                                          
 
                                        



                              





                                           
                                     
                                             



                                               
                                




                                



                                                             
                                                                 

                              






                                                                                   



                                                      

                 

 
                                                   


                                                    
                                


                      












                                                  
                                                                
                        


                              

                                                
                                             
                        




                                                                                                       
                         
                              

                 

                                          

                                                        
                                                      

                                          
                                         

                                                             
                                                           































                                                                                                                 














                                                                                                                                                                     
 























                                                                                                                                          
                              


                                                                                                                                               
                                                                                               
                                                   
                                                                                                 





                                                                    















                                                                                








                                                                                                                                                      
         
 










                                                     
         
                                         

















                                                           






                                                   




                                
                                               







                                                                                   







                                                       





                                            






                                     
                         
                                                      
                

                      
                                                                                   
                
                      


                       
                          
                                           
                                                        
                                          
                                              
                                            
                 
                     
                                    
                                                   
                    
                                                
                                                                                

                                                                            
                                                        

                                                                                       



                                                                             
                                                                   
                                                            


                                                                       

                         

         
 
                                                               




                                  
                                              
















                                                         
                                               




                              






                                       
                                                                     


                                 





                                            
                                                     
 
 



                                                       

                              



                                                                 
                      


                                   





                                                                            












                                                    













                                                             
 






















                                                                                                                


                                                  

                                       
                                                            
                                                             
                 




                                                   



                                        
                                  




                                                                                 
                                                                 



                                          
                                      




                                                                                
                                                                                                 



                                          
                                        




                                                                                 
                                                                       

                                          




                                                                                
                                                                                         

                                          

                          
                                                          




                                                                                    
                                                                     


                                                                                  
                                                                    



                                          
                                   




                                                                                
                                                                           



                                          
                                    




                                                                                 
                                                                   



                                          





                                                    
                                                       



















                                                                                                                  
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/telegram"
	"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 := gateway.SplitJID(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
			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 {
				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:]
				}
			}
			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
				}
			}

			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)
					} */
				} 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.SendServiceMessage(msg.From, response, component)
				}
				return
			}
		}
		log.Warn("Unknown purpose of the message, skipping")
	}

	if msg.Body == "" {
		var privilege extensions.ComponentPrivilege
		if ok := msg.Get(&privilege); ok {
			log.Debugf("privilege: %#v", privilege)
		}

		for _, perm := range privilege.Perms {
			if perm.Access == "message" && perm.Type == "outgoing" {
				gateway.MessageOutgoingPermission = true
			}
		}
	}

	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() {
					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
	}
	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 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 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 {
		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 = info.Fn
		if base64Photo != "" {
			vcard.Photo.Type.Text = "image/jpeg"
			vcard.Photo.Binval.Text = base64Photo
		}
		vcard.Nickname.Text = info.Nickname
		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,
					},
				},
			})
		}
		if info.Nickname != "" {
			nodes = append(nodes, stanza.Node{
				XMLName: xml.Name{Local: "nickname"},
				Nodes: []stanza.Node{
					stanza.Node{
						XMLName: xml.Name{Local: "text"},
						Content: info.Nickname,
					},
				},
			}, stanza.Node{
				XMLName: xml.Name{Local: "impp"},
				Nodes: []stanza.Node{
					stanza.Node{
						XMLName: xml.Name{Local: "uri"},
						Content: "https://t.me/" + info.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: 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
}