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



                      
                               
                 
              
              
 
                                                    
                                                             

                                        
                                  



                              


                     

                     

 

                                                       
                                       
                                             
                            
 
                                        
                   
 


                                                                                 



                                                                  

                                                                                                         
 
                                                 

                                                                                                                                                        


                                                                              

                                                                                                


                                                            

                                                                                                  


                                                                     

                                                                                                                                                                           

 





                                                                                                                                                                                                  








                                                             



                                  
                            
                        
                              








                                                               
         
                     




                                                         





                                  






                                                         


                                          
                                        
                                          
                                 
                  

                                 

         




                                                                           




                                                                                 


                                                                                                                            
         
                                                              

                                                                                    













                                                                                              









                                                                                            


                                                                                          
 
                     



                                                          
                                                  












                                                                                                  












                                                                                                                         



                                                         




















                                                                                      
                                                     
















                                                                                                                









                                                       
                                             
 

                                           

                                                           



















                                                


                                     


                                              




                                                 
 


                                                         



                                                                             





                                                              









                                                        



                                                                  

                               



                                                                 

                                 



                                                

                                   





                                                                                                           

                                







                                                                                                                   
         

                                                         
                                      
                                                                        



                                                                                   
                         


                                                                     








                                                                                                                      

                 




                                                   
                                                                               



























                                                         
                                                         
                               
                                                 


                                  
                                
                                                            
                                  



                  
















                                                                                  
 





                                                                       










                                                               









                                                 
package gateway

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

	"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
}

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, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, isCarbon, isGroupchat bool, originalFrom string) {
	sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, "", isCarbon, isGroupchat, false, originalFrom)
}

// SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to, body string, component *xmpp.Component) {
	sendMessageWrapper(to, "", body, "", "", component, nil, 0, "", false, false, false, "")
}

// SendTextMessage creates and sends a simple message stanza
func SendTextMessage(to, from, body string, component *xmpp.Component) {
	sendMessageWrapper(to, from, body, "", "", component, nil, 0, "", false, false, false, "")
}

// SendMessageWithOOB creates and sends a message stanza with OOB URL
func SendMessageWithOOB(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat bool, originalFrom string) {
	sendMessageWrapper(to, from, body, "", id, component, reply, timestamp, oob, isCarbon, isGroupchat, false, originalFrom)
}

// SendSubjectMessage creates and sends a MUC subject
func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) {
	sendMessageWrapper(to, from, "", subject, id, component, nil, timestamp, "", false, true, true, "")
}

func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string) {
	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 isGroupchat {
		logFrom = from
		messageFrom = from
	} else {
		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")

	var messageType stanza.StanzaType
	if isGroupchat {
		messageType = stanza.MessageTypeGroupchat
	} else {
		messageType = stanza.MessageTypeChat
	}

	message := stanza.Message{
		Attrs: stanza.Attrs{
			From: messageFrom,
			To:   messageTo,
			Type: messageType,
			Id:   id,
		},
		Subject: subject,
		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 !isGroupchat && !isCarbon && toJid.Resource != "" {
		message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
	}
	if timestamp != 0 {
		var delayFrom string
		if isGroupchat {
			delayFrom, _, _ = SplitJID(from)
		}
		message.Extensions = append(message.Extensions, extensions.MessageDelay{
			From:  delayFrom,
			Stamp: time.Unix(timestamp, 0).UTC().Format(time.RFC3339),
		})
		message.Extensions = append(message.Extensions, extensions.MessageDelayLegacy{
			From:  delayFrom,
			Stamp: time.Unix(timestamp, 0).UTC().Format("20060102T15:04:05"),
		})
	}
	if originalFrom != "" {
		message.Extensions = append(message.Extensions, extensions.MessageAddresses{
			Addresses: []extensions.MessageAddress{
				extensions.MessageAddress{
					Type: "ofrom",
					Jid:  originalFrom,
				},
			},
		})
	}
	if subject == "" && forceSubject {
		message.Extensions = append(message.Extensions, extensions.EmptySubject{})
	}

	if isCarbon {
		carbonMessage := extensions.ClientMessage{
			Attrs: stanza.Attrs{
				From: bareTo,
				To:   to,
				Type: messageType,
			},
		}
		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))

// SPMUCAffiliation is a XEP-0045 MUC affiliation
var SPMUCAffiliation = args.NewString()

// SPMUCJid is a real jid of a MUC member
var SPMUCJid = args.NewString()

// SPMUCStatusCodes is a set of XEP-0045 MUC status codes
var SPMUCStatusCodes = args.New()

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,
				},
			})
		}
	}
	if SPMUCAffiliation.IsSet(args) {
		affiliation := SPMUCAffiliation.Get(args)
		if affiliation != "" {
			userExt := extensions.PresenceXMucUserExtension{
				Item: extensions.PresenceXMucUserItem{
					Affiliation: affiliation,
					Role:        affilationToRole(affiliation),
				},
			}
			if SPMUCJid.IsSet(args) {
				userExt.Item.Jid = SPMUCJid.Get(args)
			}
			if SPMUCStatusCodes.IsSet(args) {
				statusCodes := SPMUCStatusCodes.Get(args).([]uint16)
				for _, statusCode := range statusCodes {
					userExt.Statuses = append(userExt.Statuses, extensions.PresenceXMucUserStatus{
						Code: statusCode,
					})
				}
			}
			presence.Extensions = append(presence.Extensions, userExt)
		}
	}

	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
}

// 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
}

func affilationToRole(affilation string) string {
	switch affilation {
	case "owner", "admin":
		return "moderator"
	case "member":
		return "participant"
	}
	return "none"
}