telegabber/xmpp/gateway/gateway.go
2026-07-27 22:58:48 -04:00

960 lines
25 KiB
Go

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"
"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 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. See the SM* option
// constructors below (SMBody, SMReply, SMOOB, ...) for what can be set.
func SendMessage(to, from string, component *xmpp.Component, args ...args.V) {
sendMessageWrapper(to, from, component, args...)
}
// 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, "", component, SMBody(body), SMId(id))
}
// 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, component, SMBody(body), SMId(id), SMIsGroupchat(isGroupchat))
}
// 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, component, SMBody(body), SMId(id), SMIsGroupchat(true))
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, component, SMErrorText(text), SMIsGroupchat(isGroupchat), SMErrorCode(code))
}
// 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, component, SMBody(body), SMErrorText(errorText), SMId(id), SMIsGroupchat(isGroupchat), SMErrorCode(code))
}
// SendSubjectMessage creates and sends a MUC subject
func SendSubjectMessage(to, from, subject, id string, component *xmpp.Component, timestamp int64) {
sendMessageWrapper(to, from, component, SMSubject(subject), SMId(id), SMTimestamp(timestamp), SMIsGroupchat(true), SMForceSubject(true))
}
// 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, SMMarker(&marker{
Type: markerType,
Id: markerId,
}))
}
// SendMUCInvite creates and send a MUC invitation message
func SendMUCInvite(to string, from string, component *xmpp.Component, inviteFrom string) {
sendMessageWrapper(to, from, component, SMInviteFrom(inviteFrom))
}
// 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, SMIsGroupchat(true), SMStatusCode(statusCode))
}
// SMBody is the message body
var SMBody = args.NewString()
// SMSubject is a MUC subject
var SMSubject = args.NewString()
// SMErrorText is the human-readable error text
var SMErrorText = args.NewString()
// SMId is the stanza id
var SMId = args.NewString()
// SMReply is a XEP-0461 reply reference (*Reply)
var SMReply = args.New()
// SMMarker is a XEP-0333 marker (*marker)
var SMMarker = args.New()
// SMTimestamp is a XEP-0203 delay timestamp
var SMTimestamp = args.NewInt64()
// SMOOB is a XEP-0066 out of band URL
var SMOOB = args.NewString()
// SMReplaceId is a XEP-0308 replaced message id
var SMReplaceId = args.NewString()
// SMIsCarbon marks the message as a XEP-0280 carbon copy
var SMIsCarbon = args.NewBool()
// SMIsGroupchat marks the message as a groupchat message
var SMIsGroupchat = args.NewBool()
// SMForceSubject forces an empty MUC subject element to be included
var SMForceSubject = args.NewBool()
// SMRequestReceipt requests a XEP-0184 receipt
var SMRequestReceipt = args.NewBool()
// SMOriginalFrom is a XEP-0033 original sender address
var SMOriginalFrom = args.NewString()
// SMErrorCode is a legacy numeric error code
var SMErrorCode = args.NewInt()
// SMInviteFrom is a XEP-0045 MUC direct invitation sender
var SMInviteFrom = args.NewString()
// SMStanzaId is a XEP-0359 stanza id
var SMStanzaId = args.NewString()
// SMStatusCode is a XEP-0045 muc#user status code
var SMStatusCode = args.NewInt64()
// SMMamQueryId is a XEP-0313 MAM query id (namespace-prefixed, space-separated)
var SMMamQueryId = args.NewString()
// SMMucJID is the real MUC room JID to send to
var SMMucJID = args.NewString()
// SMMucUserItem is a XEP-0045 muc#user item (*MUCUserItem)
var SMMucUserItem = args.New()
func sendMessageWrapper(to, from string, component *xmpp.Component, args ...args.V) {
body := SMBody.Get(args)
subject := SMSubject.Get(args)
errorText := SMErrorText.Get(args)
id := SMId.Get(args)
reply, _ := SMReply.Get(args).(*Reply)
marker, _ := SMMarker.Get(args).(*marker)
timestamp := SMTimestamp.Get(args)
oob := SMOOB.Get(args)
replaceId := SMReplaceId.Get(args)
isCarbon := SMIsCarbon.Get(args)
isGroupchat := SMIsGroupchat.Get(args)
forceSubject := SMForceSubject.Get(args)
requestReceipt := SMRequestReceipt.Get(args)
originalFrom := SMOriginalFrom.Get(args)
errorCode := SMErrorCode.Get(args)
inviteFrom := SMInviteFrom.Get(args)
stanzaId := SMStanzaId.Get(args)
statusCode := SMStatusCode.Get(args)
mamQueryId := SMMamQueryId.Get(args)
mucJID := SMMucJID.Get(args)
mucUserItem, _ := SMMucUserItem.Get(args).(*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))
// 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 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
}
// isSubscriptionType reports whether a presence type manages the subscription
// state. Per RFC 6121 § 3 such presences must be addressed from a bare JID;
// attaching the component resource makes clients treat the full JID as a
// separate entity and produces a duplicate subscription request.
func isSubscriptionType(typ string) bool {
switch typ {
case "subscribe", "subscribed", "unsubscribe", "unsubscribed":
return true
}
return false
}
// SimplePresence crafts simple presence varargs
func SimplePresence(from int64, typ string) []args.V {
args := []args.V{SPType(typ)}
if isSubscriptionType(typ) {
// subscription presences must come from the bare JID
args = append(args, SPFrom(CHATNODE(from)))
} else {
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
}
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)
}
func InviteToMUC(chatID int64, jid string, component *xmpp.Component) {
SendMUCInvite(jid, MUCNODE(chatID), component, Jid.Full())
}