aboutsummaryrefslogblamecommitdiff
path: root/xmpp/gateway/gateway.go
blob: 736f7606762415d195362a548862b947b813c6fa (plain) (tree)
1
2
3
4
5
6
7
8
9



                      
                               
                 
                 
              
 
                                                    
                                                             

                                        
                                  



                              


                     

                     

 










                                            

                                                       
                                       
                                             
                            
 
                                        
                   
 


                                                                                 



                                                                  

                                                                                                         
 
                                                 

                                                                                                                                                            



                                                                              
                                                                                       



                                                                                      
                                                                                         


                                                                     

                                                                                                                                                                        

 




                                                                                                                   
                                

 
                                                                                                                                                                                        








                                                             



                                  
                            






                                                       
                     




                                                         








                                          
                                        
                                     
                                 



                           




                                                                           




                                                                                 


                                                                                                                            
         




                                                                                                            
                                                                                                              

                 


                                                                                    
                           
                                                                                  
         


                                                                                                  
 
                     

















                                                                                                  












                                                                                                                         



                                                         




















                                                                                      
                                                     
















                                                                                                                









                                                       
                                             
 

                                           

                                                           



















                                                


                                     






                                                                             





                                                              









                                                        



                                                                  

                               



                                                                 

                                 



                                                

                                   





                                                                                                           

                                







                                                                                                                   





                                                   
                                                                               



























                                                         
                                                         
                               
                                                 


                                  
                                
                                                            
                                  



                  
 













                                                                     















                                                                                  
 





                                                                       










                                                               
package gateway

import (
	"encoding/xml"
	"github.com/pkg/errors"
	"strconv"
	"strings"
	"sync"

	"dev.narayana.im/narayana/telegabber/badger"
	"dev.narayana.im/narayana/telegabber/xmpp/extensions"

	log "github.com/sirupsen/logrus"
	"github.com/soheilhy/args"
	"gosrc.io/xmpp"
	"gosrc.io/xmpp/stanza"
)

type Reply struct {
	Author string
	Id     string
	Start  uint64
	End    uint64
}

type MarkerType byte
const (
	MarkerTypeReceived MarkerType = iota
	MarkerTypeDisplayed
)

type marker struct {
	Type MarkerType
	Id   string
}

const NSNick string = "http://jabber.org/protocol/nick"

// Queue stores presences to send later
var Queue = make(map[string]*stanza.Presence)
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, id string, component *xmpp.Component, reply *Reply, replaceId string, isCarbon, requestReceipt bool) {
	sendMessageWrapper(to, from, body, id, component, reply, nil, "", replaceId, isCarbon, requestReceipt)
}

// SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to string, body string, component *xmpp.Component) {
	sendMessageWrapper(to, "", body, "", component, nil, nil, "", "", false, 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, nil, "", "", false, 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, replaceId string, isCarbon, requestReceipt bool) {
	sendMessageWrapper(to, from, body, id, component, reply, nil, oob, replaceId, isCarbon, requestReceipt)
}

// SendMessageMarker creates and sends a message stanza with a XEP-0333 marker
func SendMessageMarker(to string, from string, component *xmpp.Component, markerType MarkerType, markerId string) {
	sendMessageWrapper(to, from, "", "", component, nil, &marker{
		Type: markerType,
		Id:   markerId,
	}, "", "", false, false)
}

func sendMessageWrapper(to string, from string, body string, id string, component *xmpp.Component, reply *Reply, marker *marker, oob, replaceId string, isCarbon, requestReceipt 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
	} else {
		logFrom = from
		messageFrom = from + "@" + componentJid
	}
	if isCarbon {
		messageTo = messageFrom
		messageFrom = bareTo + "/" + Jid.Resource
	} else {
		messageTo = to
	}

	log.WithFields(log.Fields{
		"from": logFrom,
		"to":   to,
	}).Warn("Got message")

	message := stanza.Message{
		Attrs: stanza.Attrs{
			From: messageFrom,
			To:   messageTo,
			Type: "chat",
			Id:   id,
		},
		Body: body,
	}

	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 marker != nil {
		if marker.Type == MarkerTypeReceived {
			message.Extensions = append(message.Extensions, stanza.MarkReceived{ID: marker.Id})
		} else if marker.Type == MarkerTypeDisplayed {
			message.Extensions = append(message.Extensions, stanza.MarkDisplayed{ID: marker.Id})
			message.Extensions = append(message.Extensions, stanza.ReceiptReceived{ID: marker.Id})
		}
	}
	if !isCarbon && toJid.Resource != "" {
		message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
	}
	if requestReceipt {
		message.Extensions = append(message.Extensions, stanza.Markable{})
	}
	if replaceId != "" {
		message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId})
	}

	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
func SetNickname(to string, from string, nickname string, component *xmpp.Component) {
	componentJid := Jid.Bare()
	messageFrom := from + "@" + componentJid

	log.WithFields(log.Fields{
		"from": from,
		"to":   to,
	}).Warn("Set nickname")

	message := stanza.Message{
		Attrs: stanza.Attrs{
			From: messageFrom,
			To:   to,
			Type: "headline",
		},
		Extensions: []stanza.MsgExtension{
			stanza.PubSubEvent{
				EventElement: stanza.ItemsEvent{
					Node: NSNick,
					Items: []stanza.ItemEvent{
						stanza.ItemEvent{
							Any: &stanza.Node{
								XMLName: xml.Name{Space: NSNick, Local: "nick"},
								Content: nickname,
							},
						},
					},
				},
			},
		},
	}

	sendMessage(&message, component)
}

func sendMessage(message *stanza.Message, component *xmpp.Component) {
	// explicit check, as marshalling is expensive
	if log.GetLevel() == log.DebugLevel {
		xmlMessage, err := xml.Marshal(message)
		if err == nil {
			log.Debug(string(xmlMessage))
		} else {
			log.Debugf("%#v", message)
		}
	}

	_ = ResumableSend(component, message)
}

// LogBadPresence verbosely logs a presence
func LogBadPresence(presence *stanza.Presence) {
	log.Errorf("Couldn't send presence: %#v", presence)
}

// SPFrom is a Telegram user id
var SPFrom = args.NewString()

// SPType is a presence type
var SPType = args.NewString()

// SPShow is a availability status
var SPShow = args.NewString()

// SPStatus is a verbose status
var SPStatus = args.NewString()

// SPNickname is a XEP-0172 nickname
var SPNickname = args.NewString()

// SPPhoto is a XEP-0153 hash of avatar in vCard
var SPPhoto = args.NewString()

// SPResource is an optional resource
var SPResource = args.NewString()

// SPImmed skips queueing
var SPImmed = args.NewBool(args.Default(true))

func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
	var presenceFrom string
	if SPFrom.IsSet(args) {
		presenceFrom = SPFrom.Get(args) + "@" + bareJid
		if SPResource.IsSet(args) {
			resource := SPResource.Get(args)
			if resource != "" {
				presenceFrom += "/" + resource
			}
		}
	} else {
		presenceFrom = bareJid
	}

	presence := stanza.Presence{Attrs: stanza.Attrs{
		From: presenceFrom,
		To:   to,
	}}

	if SPType.IsSet(args) {
		t := SPType.Get(args)
		if t != "" {
			presence.Attrs.Type = stanza.StanzaType(t)
		}
	}
	if SPShow.IsSet(args) {
		show := SPShow.Get(args)
		if show != "" {
			presence.Show = stanza.PresenceShow(show)
		}
	}
	if SPStatus.IsSet(args) {
		status := SPStatus.Get(args)
		if status != "" {
			presence.Status = status
		}
	}
	if SPNickname.IsSet(args) {
		nickname := SPNickname.Get(args)
		if nickname != "" {
			presence.Extensions = append(presence.Extensions, extensions.PresenceNickExtension{
				Text: nickname,
			})
		}
	}
	if SPPhoto.IsSet(args) {
		photo := SPPhoto.Get(args)
		if photo != "" {
			presence.Extensions = append(presence.Extensions, extensions.PresenceXVCardUpdateExtension{
				Photo: extensions.PresenceXVCardUpdatePhoto{
					Text: photo,
				},
			})
		}
	}

	return presence
}

// SendPresence creates and sends a presence stanza
func SendPresence(component *xmpp.Component, to string, args ...args.V) error {
	var logFrom string
	bareJid := Jid.Bare()
	if SPFrom.IsSet(args) {
		logFrom = SPFrom.Get(args)
	} else {
		logFrom = bareJid
	}

	log.WithFields(log.Fields{
		"type": SPType.Get(args),
		"from": logFrom,
		"to":   to,
	}).Info("Got presence")

	presence := newPresence(bareJid, to, args...)

	// explicit check, as marshalling is expensive
	if log.GetLevel() == log.DebugLevel {
		xmlPresence, err := xml.Marshal(presence)
		if err == nil {
			log.Debug(string(xmlPresence))
		} else {
			log.Debugf("%#v", presence)
		}
	}

	immed := SPImmed.Get(args)
	if immed {
		err := ResumableSend(component, presence)
		if err != nil {
			LogBadPresence(&presence)
			return err
		}
	} else {
		QueueLock.Lock()
		Queue[presence.From+presence.To] = &presence
		QueueLock.Unlock()
	}

	return nil
}

// SPAppendFrom appends numeric from and resource to varargs
func SPAppendFrom(oldArgs []args.V, id int64) []args.V {
	newArgs := append(oldArgs, SPFrom(strconv.FormatInt(id, 10)))
	newArgs = append(newArgs, SPResource(Jid.Resource))
	return newArgs
}

// SimplePresence crafts simple presence varargs
func SimplePresence(from int64, typ string) []args.V {
	args := []args.V{SPType(typ)}
	args = SPAppendFrom(args, from)
	return args
}

// ResumableSend tries to resume the connection once and sends the packet again
func ResumableSend(component *xmpp.Component, packet stanza.Packet) error {
	err := component.Send(packet)
	if err != nil && strings.HasPrefix(err.Error(), "cannot send packet") {
		log.Warn("Packet send failed, trying to resume the connection...")
		err = component.Connect()
		if err == nil {
			err = component.Send(packet)
		}
	}

	if err != nil {
		log.Error(err.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
}