telegabber/xmpp/gateway/gateway.go

500 lines
13 KiB
Go

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, 0)
}
// 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, "", 0)
}
// 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, "", 0)
}
// SendErrorMessage creates and sends an error message stanza
func SendErrorMessage(to, from, text string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, text, "", "", component, nil, 0, "", false, isGroupchat, false, "", code)
}
// 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, 0)
}
// 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, "", 0)
}
func sendMessageWrapper(to, from, body, subject, id string, component *xmpp.Component, reply *Reply, timestamp int64, oob string, isCarbon, isGroupchat, forceSubject bool, originalFrom string, errorCode int) {
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 errorCode != 0 {
messageType = stanza.MessageTypeError
} else 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,
}
if errorCode == 0 {
message.Body = body
} else {
message.Error = stanza.Err{
Code: errorCode,
Text: body,
}
switch errorCode {
case 400:
message.Error.Type = stanza.ErrorTypeModify
message.Error.Reason = "bad-request"
case 403:
message.Error.Type = stanza.ErrorTypeAuth
message.Error.Reason = "forbidden"
case 404:
message.Error.Type = stanza.ErrorTypeCancel
message.Error.Reason = "item-not-found"
case 500:
message.Error.Type = stanza.ErrorTypeWait
message.Error.Reason = "internal-server-error"
default:
log.Error("Unknown error code, falling back with empty reason")
message.Error.Type = stanza.ErrorTypeCancel
message.Error.Reason = "undefined-condition"
}
}
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"
}