telegabber/xmpp/gateway/gateway.go
Bohdan Horbeshko 0e6fbc652f Merge branch 'muc' into calls
( and finished Caps support, sorry for the mess :-' )
2026-03-25 13:29:33 -04:00

1157 lines
31 KiB
Go

package gateway
import (
"crypto/sha1"
"encoding/base64"
"encoding/xml"
"github.com/pkg/errors"
"fmt"
"io"
"sort"
"strconv"
"strings"
"sync"
"dev.narayana.im/narayana/telegabber/badger"
"dev.narayana.im/narayana/telegabber/xmpp/extensions"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/soheilhy/args"
"github.com/xdg-go/stringprep"
"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
}
type MUCUserItem struct {
Affiliation string
Jid string
Role string
}
const NSNick string = "http://jabber.org/protocol/nick"
const NodeVCard4 string = "urn:xmpp:vcard4"
const NodeAvatarMetadata string = "urn:xmpp:avatar:metadata"
const NodeAvatarMetadataNotify string = NodeAvatarMetadata + "+notify"
const NodeAvatarData string = "urn:xmpp:avatar:data"
const NSCommand string = "http://jabber.org/protocol/commands"
const capsNode string = "https://dev.narayana.im/narayana/telegabber/"
const NS_MAM2 = "urn:xmpp:mam:2"
const NS_MAM1 = "urn:xmpp:mam:1"
const NS_MAM0 = "urn:xmpp:mam:0"
// 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
// Version stores this software's version
var Version string
// 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
// MAMThreshold specifies a day limit behind which history should not be requested to avoid abuse detection and storage overload
var MAMThreshold uint32
// CHATNODE converts numeric id to node part of 1-1 chat JID
func CHATNODE(chatId int64) string {
return strconv.FormatInt(chatId, 10)
}
// CHATJID converts numeric id to 1-1 chat JID
func CHATJID(chatId int64, full bool) string {
var suffix string
if full {
suffix = Jid.Full()
} else {
suffix = Jid.Bare()
}
return CHATNODE(chatId) + "@" + suffix
}
// MUCNODE converts numeric id to node part of MUC JID
func MUCNODE(chatId int64) string {
return "c" + CHATNODE(chatId)
}
// MUCJID converts numeric id to MUC JID
func MUCJID(chatId int64) string {
return "c" + CHATJID(chatId, false)
}
var resourcePrepProfile = stringprep.Profile{
Mappings: []stringprep.Mapping{
stringprep.TableB1,
},
Normalize: true,
Prohibits: []stringprep.Set{
stringprep.TableC1_2,
stringprep.TableC2_1,
stringprep.TableC2_2,
stringprep.TableC3,
stringprep.TableC4,
stringprep.TableC5,
stringprep.TableC6,
stringprep.TableC7,
stringprep.TableC8,
stringprep.TableC9,
},
CheckBiDi: true,
}
// ResourcePrep normalizes a resource according to RFC 6122
func ResourcePrep(resource string) (string, error) {
return resourcePrepProfile.Prepare(resource)
}
// SendMessage creates and sends a message stanza
func SendMessage(to, from, body, id string, component *xmpp.Component, reply *Reply, timestamp int64, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId, mamQueryId, mucJID string, mucUserItem *MUCUserItem) {
sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, "", replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0, mamQueryId, mucJID, mucUserItem)
}
// SendServiceMessage creates and sends a simple message stanza from transport
func SendServiceMessage(to, body string, component *xmpp.Component) {
var id string
if uuid, err := uuid.NewRandom(); err == nil {
id = uuid.String()
}
sendMessageWrapper(to, "", body, "", "", id, component, nil, nil, 0, "", "", false, false, false, false, "", 0, "", "", 0, "", "", nil)
}
// SendTextMessage creates and sends a simple message stanza
func SendTextMessage(to, from, body string, component *xmpp.Component, isGroupchat bool) {
var id string
if uuid, err := uuid.NewRandom(); err == nil {
id = uuid.String()
}
sendMessageWrapper(to, from, body, "", "", id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", 0, "", "", 0, "", "", nil)
}
// SendMUCAnnouncement creates and sends a message by a temporary occupant
func SendMUCAnnouncement(to, from, body, nickname, id string, component *xmpp.Component) {
if nickname == "" {
nickname = "announcement"
}
fullFrom := from + "/" + nickname
SendPresence(
component,
to,
SPFullFrom(fullFrom),
SPMUCAffiliation("admin"),
SPMUCRole("moderator"),
SPMUCJid(from),
)
if id == "" {
if uuid, err := uuid.NewRandom(); err == nil {
id = uuid.String()
}
}
sendMessageWrapper(to, fullFrom, body, "", "", id, component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", 0, "", "", nil)
SendPresence(
component,
to,
SPType("unavailable"),
SPFullFrom(fullFrom),
SPMUCAffiliation("none"),
SPMUCRole("none"),
SPMUCJid(from),
)
}
// 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, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0, "", "", nil)
}
// SendErrorMessageWithBody creates and sends an error message stanza with body payload
func SendErrorMessageWithBody(to, from, body, errorText, id string, code int, isGroupchat bool, component *xmpp.Component) {
sendMessageWrapper(to, from, body, "", errorText, id, component, nil, nil, 0, "", "", false, isGroupchat, false, false, "", code, "", "", 0, "", "", nil)
}
// 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, replaceId string, isCarbon, isGroupchat, requestReceipt bool, originalFrom, stanzaId, mamQueryId, mucJID string, mucUserItem *MUCUserItem) {
sendMessageWrapper(to, from, body, "", "", id, component, reply, nil, timestamp, oob, replaceId, isCarbon, isGroupchat, false, requestReceipt, originalFrom, 0, "", stanzaId, 0, mamQueryId, mucJID, mucUserItem)
}
// 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, nil, timestamp, "", "", false, true, true, false, "", 0, "", "", 0, "", "", nil)
}
// 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,
}, 0, "", "", false, false, false, false, "", 0, "", "", 0, "", "", nil)
}
// SendMUCInvite creates and send a MUC invitation message
func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) {
sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, false, false, false, "", 0, inviteFrom, "", 0, "", "", nil)
}
// SendMUCStatusCode creates a groupchat message with a muc#user status code
func SendMUCStatusCode(to string, from string, component *xmpp.Component, statusCode int64) {
sendMessageWrapper(to, from, "", "", "", "", component, nil, nil, 0, "", "", false, true, false, false, "", 0, "", "", statusCode, "", "", nil)
}
func sendMessageWrapper(to, from, body, subject, errorText, id string, component *xmpp.Component, reply *Reply, marker *marker, timestamp int64, oob, replaceId string, isCarbon, isGroupchat, forceSubject, requestReceipt bool, originalFrom string, errorCode int, inviteFrom, stanzaId string, statusCode int64, mamQueryId, mucJID string, mucUserItem *MUCUserItem) {
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
var bareFrom string
if isGroupchat {
logFrom = from
messageFrom = from
bareFrom, _, _ = SplitJID(from)
} else {
if from == "" {
logFrom = componentJid
messageFrom = componentJid
bareFrom = componentJid
} else if inviteFrom != "" {
logFrom = from
messageFrom = from + "@" + Jid.Bare()
bareFrom = messageFrom
} else {
logFrom = from
messageFrom = from + "@" + componentJid
bareFrom = from + "@" + Jid.Bare()
}
}
if isCarbon {
messageTo = messageFrom
messageFrom = bareTo + "/" + Jid.Resource
} else if mucJID != "" {
messageTo = mucJID
} 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 if inviteFrom != "" {
messageType = stanza.MessageTypeNormal
} else {
messageType = stanza.MessageTypeChat
}
message := stanza.Message{
Attrs: stanza.Attrs{
From: messageFrom,
To: messageTo,
Type: messageType,
Id: id,
},
Subject: subject,
Body: body,
}
if errorCode != 0 {
message.Error = stanza.Err{
Code: errorCode,
Text: errorText,
}
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 406:
message.Error.Type = stanza.ErrorTypeModify
message.Error.Reason = "not-acceptable"
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 != "" && inviteFrom == "" {
message.Extensions = append(message.Extensions, stanza.HintNoCopy{})
}
if timestamp != 0 && mamQueryId == "" {
var delayFrom string
if isGroupchat {
delayFrom = bareFrom
}
message.Extensions = append(message.Extensions, extensions.NewMessageDelay(timestamp, delayFrom))
message.Extensions = append(message.Extensions, extensions.NewMessageDelayLegacy(timestamp, delayFrom))
}
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 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 requestReceipt {
message.Extensions = append(message.Extensions, stanza.Markable{})
}
if replaceId != "" {
message.Extensions = append(message.Extensions, extensions.Replace{Id: replaceId})
}
var userExt extensions.MessageXMucUserExtension
if inviteFrom != "" {
userExt.Invite = &extensions.MessageXMucUserInvite{
From: inviteFrom,
}
message.Extensions = append(message.Extensions, extensions.MessageXLegacyInviteExtension{
Jid: messageFrom,
})
}
if statusCode != 0 {
userExt.Status = &extensions.MessageXMucUserStatus{
Code: strconv.FormatInt(statusCode, 10),
}
}
if mucUserItem != nil {
userExt.Item = extensions.PresenceXMucUserItem{
Affiliation: mucUserItem.Affiliation,
Jid: &mucUserItem.Jid,
Role: mucUserItem.Role,
}
}
if inviteFrom != "" || statusCode != 0 || mucUserItem != nil {
message.Extensions = append(message.Extensions, userExt)
}
if stanzaId != "" {
message.Extensions = append(message.Extensions, extensions.MessageStanzaId{
Id: stanzaId,
By: bareFrom,
})
if stanzaId != id {
message.Extensions = append(message.Extensions, extensions.MessageOriginId{
Id: id,
})
}
}
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 if mamQueryId != "" {
mneVpadluProbrasyvatJoshParametryPoraRefaktorit := strings.Split(mamQueryId, " ")
ns := mneVpadluProbrasyvatJoshParametryPoraRefaktorit[0]
mamQueryId = mneVpadluProbrasyvatJoshParametryPoraRefaktorit[1]
delay := extensions.NewMessageDelay(timestamp, "")
clientMessage := extensions.ClientMessage{
Attrs: message.Attrs,
Subject: message.Subject,
Body: message.Body,
Thread: message.Thread,
Error: message.Error,
Extensions: message.Extensions,
}
forwarded := extensions.ForwardedMessage{
Delay: &delay,
Message: &clientMessage,
}
var ext stanza.MsgExtension
switch ns {
case NS_MAM2:
ext = extensions.MAM2MessageResult{
Id: stanzaId,
QueryId: mamQueryId,
Forwarded: &forwarded,
}
case NS_MAM1:
ext = extensions.MAM1MessageResult{
Id: stanzaId,
QueryId: mamQueryId,
Forwarded: &forwarded,
}
case NS_MAM0:
ext = extensions.MAM0MessageResult{
Id: stanzaId,
QueryId: mamQueryId,
Forwarded: &forwarded,
}
}
mamMessage := stanza.Message{
Attrs: stanza.Attrs{
From: mucJID,
To: to,
Type: messageType,
},
Extensions: []stanza.MsgExtension{ext},
}
sendMessage(&mamMessage, 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()
// SPFullFrom is for specifying a full from when desired
var SPFullFrom = 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))
// SPCaps is a XEP-0115 verification string
var SPCaps = args.NewString()
// SPMUCAffiliation is a XEP-0045 MUC affiliation
var SPMUCAffiliation = args.NewString()
// SPMUCRole is a XEP-0045 MUC role
var SPMUCRole = args.NewString()
// SPMUCNick is a XEP-0045 MUC user nick
var SPMUCNick = 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()
// SPMUCDestroy is a XEP-0045 room destruction element
var SPMUCDestroy = args.NewString()
// SPToJids achieves to send the presence to certain full jids only
var SPToJids = args.New()
func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
var presenceFrom string
if SPFullFrom.IsSet(args) {
presenceFrom = SPFullFrom.Get(args)
} else 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 SPCaps.IsSet(args) {
ver := SPCaps.Get(args)
if ver != "" {
presence.Extensions = append(presence.Extensions, stanza.Caps{
Hash: "sha-1",
Node: capsNode,
Ver: ver,
})
}
}
if SPMUCAffiliation.IsSet(args) {
affiliation := SPMUCAffiliation.Get(args)
if affiliation != "" {
var role string
if SPMUCRole.IsSet(args) {
role = SPMUCRole.Get(args)
} else {
role = affiliationToRole(affiliation)
}
userExt := extensions.PresenceXMucUserExtension{
Item: extensions.PresenceXMucUserItem{
Affiliation: affiliation,
Role: role,
},
}
if SPMUCNick.IsSet(args) {
userExt.Item.Nick = SPMUCNick.Get(args)
}
if SPMUCJid.IsSet(args) {
mucJid := SPMUCJid.Get(args)
userExt.Item.Jid = &mucJid
}
if SPMUCStatusCodes.IsSet(args) {
statusCodes := SPMUCStatusCodes.Get(args).([]uint16)
for _, statusCode := range statusCodes {
userExt.Statuses = append(userExt.Statuses, extensions.PresenceXMucUserStatus{
Code: statusCode,
})
}
}
if SPMUCDestroy.IsSet(args) {
userExt.Destroy = &extensions.MucDestroy{
Jid: SPMUCDestroy.Get(args),
Reason: "Group was deleted",
}
}
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")
var tos []string
if SPToJids.IsSet(args) {
tos = SPToJids.Get(args).([]string)
} else {
tos = []string{to}
}
for _, to := range tos {
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(CHATNODE(id)))
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
}
// GetDiscoInfo generates a disco info IQ query response and possibly returns error code
func GetDiscoInfo(session TelegramSession, node string, to string) (*stanza.DiscoInfo, int) {
disco := stanza.DiscoInfo{}
toID, toOk, toIsGroup := ToToID(to)
if toIsGroup {
toJid, err := stanza.NewJid(to)
if err == nil && toJid.Resource != "" {
return nil, 400
}
}
if node == "" {
var isMuc bool
if session != nil {
conf := session.GetPersistenceSession()
if conf.MUC {
if toOk && toIsGroup {
chat, _, err := session.GetContactByID(toID, nil, true)
if err == nil && session.IsGroup(chat) {
isMuc = true
disco.AddIdentity(chat.Title, "conference", "text")
disco.AddFeatures(
"http://jabber.org/protocol/muc",
"muc_persistent",
"muc_hidden",
"muc_moderated",
"muc_nonanonymous",
"muc_unsecured",
"http://jabber.org/protocol/muc#stable_id",
"jabber:iq:register",
NS_MAM0,
NS_MAM1,
NS_MAM2,
"urn:xmpp:mam:2#extended",
"urn:xmpp:sid:0",
"vcard-temp",
)
fields := []*stanza.Field{
&stanza.Field{
Var: "FORM_TYPE",
Type: "hidden",
ValuesList: []string{"http://jabber.org/protocol/muc#roominfo"},
},
&stanza.Field{
Var: "muc#roominfo_description",
Label: "Description",
ValuesList: []string{session.GetChatDescription(chat)},
},
&stanza.Field{
Var: "muc#roominfo_occupants",
Label: "Number of occupants",
ValuesList: []string{strconv.FormatInt(int64(session.GetChatMemberCount(chat)), 10)},
},
}
hashedAvatar := session.GetHashedAvatar(toID)
if hashedAvatar != nil {
fields = append(fields, &stanza.Field{
Var: "muc#roominfo_avatarhash",
Label: "Avatar hash",
ValuesList: []string{hashedAvatar.Hash},
})
}
disco.Form = stanza.NewForm(fields, "result")
}
} else if !toOk {
disco.AddFeatures(
stanza.NSDiscoItems,
"http://jabber.org/protocol/muc#stable_id",
)
disco.AddIdentity("Telegram group chats", "conference", "text")
}
}
if toOk && !toIsGroup && session.CanBeCalled(toID) {
disco.AddFeatures(
"urn:xmpp:jingle-message:0",
"urn:xmpp:jingle:1",
"urn:xmpp:jingle:apps:dtls:0",
"urn:xmpp:jingle:apps:rtp:1",
"urn:xmpp:jingle:apps:rtp:audio",
"urn:xmpp:jingle:transports:ice-udp:1",
)
}
}
if toOk {
if toIsGroup {
if !isMuc {
return nil, 404
}
} else {
disco.AddIdentity("", "account", "registered")
}
disco.AddFeatures(stanza.NSMsgChatMarkers)
disco.AddFeatures(stanza.NSMsgReceipts)
} else {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
disco.AddFeatures("jabber:iq:register")
}
disco.AddFeatures("http://jabber.org/protocol/disco#info")
disco.AddFeatures("http://jabber.org/protocol/caps")
disco.AddFeatures(NSCommand)
disco.AddFeatures("jabber:iq:version")
disco.AddFeatures("urn:xmpp:time")
} else if node == "x-roomuser-item" {
if session != nil {
conf := session.GetPersistenceSession()
if conf.MUC {
if toOk && toIsGroup {
chat, _, err := session.GetContactByID(toID, nil, true)
if err == nil && session.IsGroup(chat) {
disco.SetNode(node)
disco.AddIdentity(session.GetMUCNickname(0), "conference", "text")
}
}
}
}
} else if node == "http://jabber.org/protocol/muc#traffic" {
// noop yet, empty result as intended, TODO: add XHTML whenever supported
} else if strings.HasPrefix(node, capsNode) {
nodeParts := strings.Split(node, "#")
if session != nil && len(nodeParts) == 2 && nodeParts[0] == capsNode {
di, ok := session.GetVerDisco(nodeParts[1])
if ok && di != nil {
return di, 0
}
}
} else {
var chatType ChatType
var chatTypeErr error
if session != nil {
chatType, _, chatTypeErr = session.GetChatType(toID, true)
}
var cmdType CommandType
if toOk {
cmdType = CommandTypeChat
} else {
cmdType = CommandTypeTransport
}
for name, command := range GetCommands(cmdType) {
if node == name {
if chatTypeErr == nil && !IsCommandForChatType(command, chatType) {
break
}
disco.AddIdentity(CommandToHelpString(name, command), "automation", "command-node")
disco.AddFeatures(NSCommand, "jabber:x:data")
break
}
}
}
return &disco, 0
}
// GetCapsVer hashes a capabilities set into a verification string
func GetCapsVer(session TelegramSession, to string) (string, *stanza.DiscoInfo, int) {
disco, errorCode := GetDiscoInfo(session, "", to)
if errorCode != 0 {
return "", nil, errorCode
}
return discoToCapsHash(disco), disco, 0
}
func iOctetComparator(a, b string) bool {
return a < b
}
func discoToCaps(disco *stanza.DiscoInfo, w io.Writer) {
var identities, vars []string
capsSeparator := []byte{'<'}
for _, identity := range disco.Identity {
identities = append(identities, fmt.Sprintf(
"%s/%s//%s",
identity.Category,
identity.Type,
identity.Name,
))
}
sort.Slice(identities, func(i, j int) bool { return identities[i] < identities[j] })
for _, identity := range identities {
w.Write([]byte(identity))
w.Write(capsSeparator)
}
for _, feature := range disco.Features {
vars = append(vars, feature.Var)
}
sort.Slice(vars, func(i, j int) bool { return vars[i] < vars[j] })
for _, v := range vars {
w.Write([]byte(v))
w.Write(capsSeparator)
}
if disco.Form != nil {
fields := make([]*stanza.Field, len(disco.Form.Fields))
copy(fields, disco.Form.Fields)
sort.Slice(fields, func(i, j int) bool {
a := fields[i]
b := fields[j]
if a.Var == "FORM_TYPE" {
return true
}
if b.Var == "FORM_TYPE" {
return false
}
return a.Var < b.Var
})
for _, field := range fields {
if field.Var == "FORM_TYPE" {
if len(field.ValuesList) > 0 {
w.Write([]byte(field.ValuesList[0]))
w.Write(capsSeparator)
}
} else {
w.Write([]byte(field.Var))
w.Write(capsSeparator)
var values []string
// useless optimisation?
if len(field.ValuesList) < 2 {
values = field.ValuesList
} else {
values = make([]string, len(field.ValuesList))
copy(values, field.ValuesList)
sort.Slice(values, func(i, j int) bool { return values[i] < values[j] })
}
for _, value := range values {
w.Write([]byte(value))
w.Write(capsSeparator)
}
}
}
}
}
func discoToCapsHash(disco *stanza.DiscoInfo) string {
hash := sha1.New()
discoToCaps(disco, hash)
sha1Hash := hash.Sum(nil)
return base64.StdEncoding.EncodeToString(sha1Hash)
}
func affiliationToRole(affilation string) string {
switch affilation {
case "owner", "admin":
return "moderator"
case "member":
return "participant"
}
return "none"
}
// SendPubSubAvatarNotification encourages clients to fetch an avatar
func SendPubSubAvatarNotification(component *xmpp.Component, jid string, chatJid string, sha1 string, size int64) {
info := stanza.Node{
XMLName: xml.Name{Local: "info"},
Attrs: []xml.Attr{
xml.Attr{Name: xml.Name{Local: "bytes"}, Value: strconv.FormatInt(size, 10)},
xml.Attr{Name: xml.Name{Local: "height"}, Value: "160"},
xml.Attr{Name: xml.Name{Local: "id"}, Value: sha1},
xml.Attr{Name: xml.Name{Local: "type"}, Value: "image/jpeg"},
xml.Attr{Name: xml.Name{Local: "width"}, Value: "160"},
},
}
log.WithFields(log.Fields{
"chatJid": chatJid,
}).Debugf("%#v", info)
event := &stanza.PubSubEvent{
EventElement: &stanza.ItemsEvent{
Node: NodeAvatarMetadata,
Items: []stanza.ItemEvent{
stanza.ItemEvent{
Id: sha1,
Any: &stanza.Node{
XMLName: xml.Name{Local: "metadata", Space: NodeAvatarMetadata},
Nodes: []stanza.Node{info},
},
},
},
},
}
message := stanza.Message{
Attrs: stanza.Attrs{
From: chatJid,
To: jid,
Type: stanza.MessageTypeHeadline,
},
Extensions: []stanza.MsgExtension{event},
}
_ = ResumableSend(component, message)
}
// InviteToMUC is a MUC invite wrapper
func InviteToMUC(chatID int64, jid string, component *xmpp.Component) {
SendMUCInvite(jid, MUCNODE(chatID), component, Jid.Full())
}
// ToToID parses a JID to components
func ToToID(to string) (int64, bool, bool) {
var isGroup bool
toParts := strings.Split(to, "@")
if len(toParts) < 2 {
return 0, false, isGroup
}
node := toParts[0]
if strings.HasPrefix(node, "c") {
isGroup = true
node = node[1:]
}
toID, err := strconv.ParseInt(node, 10, 64)
if err != nil {
log.WithFields(log.Fields{
"to": to,
}).Error(errors.Wrap(err, "Invalid to JID!"))
return 0, false, isGroup
}
return toID, true, isGroup
}