mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-10-07 08:41:47 +00:00
Backport XEP-0115 support from calls branch
This commit is contained in:
parent
afb892d493
commit
a7b4eb7d88
4 changed files with 236 additions and 24 deletions
|
|
@ -609,6 +609,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
|
|||
if isMUC {
|
||||
newArgs = append(newArgs, gateway.SPFullFrom(gateway.MUCJID(chatID)))
|
||||
} else {
|
||||
newArgs = append(newArgs, gateway.SPCaps(gateway.PMCapsVer()))
|
||||
newArgs = gateway.SPAppendFrom(newArgs, chatID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@ package gateway
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -53,6 +58,12 @@ const NodeAvatarMetadataNotify string = NodeAvatarMetadata + "+notify"
|
|||
const NodeAvatarData string = "urn:xmpp:avatar:data"
|
||||
const NSCommand string = "http://jabber.org/protocol/commands"
|
||||
|
||||
// NSCaps is the XEP-0115 entity capabilities namespace
|
||||
const NSCaps string = "http://jabber.org/protocol/caps"
|
||||
|
||||
// CapsNode is this software's advertised XEP-0115 node URI
|
||||
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"
|
||||
|
|
@ -722,6 +733,9 @@ 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()
|
||||
|
||||
|
|
@ -800,6 +814,16 @@ func newPresence(bareJid string, to string, args ...args.V) stanza.Presence {
|
|||
})
|
||||
}
|
||||
}
|
||||
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 != "" {
|
||||
|
|
@ -1053,3 +1077,121 @@ func SendPubSubDeviceListNotification(component *xmpp.Component, jid string, cha
|
|||
func InviteToMUC(chatID int64, jid string, component *xmpp.Component) {
|
||||
SendMUCInvite(jid, MUCNODE(chatID), component, Jid.Full())
|
||||
}
|
||||
|
||||
// AddPMDiscoFeatures appends the disco#info identity/features for a 1:1 Telegram contact.
|
||||
func AddPMDiscoFeatures(disco *stanza.DiscoInfo) {
|
||||
disco.AddIdentity("", "account", "registered")
|
||||
if backend, ok := E2EE.Backend(); ok {
|
||||
disco.AddFeatures(backend.Namespaces().Disco...)
|
||||
}
|
||||
disco.AddFeatures(stanza.NSMsgChatMarkers)
|
||||
disco.AddFeatures(stanza.NSMsgReceipts)
|
||||
// Jingle / JMI features so clients (e.g. Dino) treat the contact as
|
||||
// call-capable. Without these, JMI <proceed> never fires: the client
|
||||
// disco-info's the caller before engaging its call state machine and
|
||||
// silently drops the call when it sees no RTP support advertised.
|
||||
disco.AddFeatures(
|
||||
"urn:xmpp:jingle:1",
|
||||
"urn:xmpp:jingle-message:0",
|
||||
"urn:xmpp:jingle:apps:rtp:1",
|
||||
"urn:xmpp:jingle:apps:rtp:audio",
|
||||
"urn:xmpp:jingle:transports:ice-udp:1",
|
||||
"urn:xmpp:jingle:apps:dtls:0",
|
||||
)
|
||||
}
|
||||
|
||||
// AddTransportDiscoFeatures appends the disco#info identity/features for the transport root JID.
|
||||
func AddTransportDiscoFeatures(disco *stanza.DiscoInfo) {
|
||||
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
|
||||
disco.AddFeatures("jabber:iq:register")
|
||||
}
|
||||
|
||||
// AddCommonDiscoFeatures appends features common to every root-node disco#info response.
|
||||
func AddCommonDiscoFeatures(disco *stanza.DiscoInfo) {
|
||||
disco.AddFeatures(
|
||||
NSCommand,
|
||||
"jabber:iq:version",
|
||||
"urn:xmpp:time",
|
||||
stanza.NSDiscoInfo,
|
||||
NSCaps,
|
||||
)
|
||||
}
|
||||
|
||||
// PMCapsVer computes the XEP-0115 ver for a 1:1 contact's disco#info.
|
||||
// Not cached: if AddPMDiscoFeatures ever starts varying silently (no
|
||||
// signature change), a cached ver would go stale with no compiler warning.
|
||||
func PMCapsVer() string {
|
||||
disco := &stanza.DiscoInfo{}
|
||||
AddPMDiscoFeatures(disco)
|
||||
AddCommonDiscoFeatures(disco)
|
||||
return DiscoVer(disco)
|
||||
}
|
||||
|
||||
// DiscoVer computes a XEP-0115 ver string for a disco#info response.
|
||||
func DiscoVer(disco *stanza.DiscoInfo) string {
|
||||
hash := sha1.New()
|
||||
discoToCaps(disco, hash)
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// discoToCaps writes disco's XEP-0115 S5.1 capabilities identifier string to w.
|
||||
func discoToCaps(disco *stanza.DiscoInfo, w io.Writer) {
|
||||
const sep = "<"
|
||||
|
||||
identities := make([]string, len(disco.Identity))
|
||||
for i, identity := range disco.Identity {
|
||||
identities[i] = fmt.Sprintf("%s/%s//%s", identity.Category, identity.Type, identity.Name)
|
||||
}
|
||||
sort.Strings(identities)
|
||||
for _, identity := range identities {
|
||||
io.WriteString(w, identity)
|
||||
io.WriteString(w, sep)
|
||||
}
|
||||
|
||||
vars := make([]string, len(disco.Features))
|
||||
for i, feature := range disco.Features {
|
||||
vars[i] = feature.Var
|
||||
}
|
||||
sort.Strings(vars)
|
||||
for _, v := range vars {
|
||||
io.WriteString(w, v)
|
||||
io.WriteString(w, sep)
|
||||
}
|
||||
|
||||
if disco.Form == nil {
|
||||
return
|
||||
}
|
||||
fields := make([]*stanza.Field, len(disco.Form.Fields))
|
||||
copy(fields, disco.Form.Fields)
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
a, b := fields[i], 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 {
|
||||
io.WriteString(w, field.ValuesList[0])
|
||||
io.WriteString(w, sep)
|
||||
}
|
||||
continue
|
||||
}
|
||||
io.WriteString(w, field.Var)
|
||||
io.WriteString(w, sep)
|
||||
|
||||
values := field.ValuesList
|
||||
if len(values) > 1 {
|
||||
values = append([]string(nil), values...)
|
||||
sort.Strings(values)
|
||||
}
|
||||
for _, value := range values {
|
||||
io.WriteString(w, value)
|
||||
io.WriteString(w, sep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package gateway
|
|||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
|
|
@ -52,3 +53,87 @@ func TestPresencePhoto(t *testing.T) {
|
|||
presence := newPresence("from@test", "to@test", SPPhoto("01b87fcd030b72895ff8e88db57ec525450f000d"))
|
||||
testPresence(t, presence, "<presence from=\"from@test\" to=\"to@test\"><x xmlns=\"vcard-temp:x:update\"><photo>01b87fcd030b72895ff8e88db57ec525450f000d</photo></x></presence>")
|
||||
}
|
||||
|
||||
func TestPresenceCaps(t *testing.T) {
|
||||
presence := newPresence("from@test", "to@test", SPCaps("QgayPKawpkPSDYmwT/WM94uAlu0="))
|
||||
testPresence(t, presence, "<presence from=\"from@test\" to=\"to@test\"><c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"https://dev.narayana.im/narayana/telegabber/\" ver=\"QgayPKawpkPSDYmwT/WM94uAlu0=\"></c></presence>")
|
||||
}
|
||||
|
||||
// Canonical XEP-0115 S5.1 worked examples (Exodus, Psi, BombusMod).
|
||||
|
||||
func testDiscoVer(t *testing.T, disco *stanza.DiscoInfo, reference string) {
|
||||
ver := DiscoVer(disco)
|
||||
if ver != reference {
|
||||
var sb strings.Builder
|
||||
discoToCaps(disco, &sb)
|
||||
t.Errorf("%v does not match %v\nRaw string: %v", ver, reference, sb.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoVerExodus(t *testing.T) {
|
||||
disco := stanza.DiscoInfo{
|
||||
Identity: []stanza.Identity{
|
||||
{Name: "Exodus 0.9.1", Category: "client", Type: "pc"},
|
||||
},
|
||||
Features: []stanza.Feature{
|
||||
{Var: "http://jabber.org/protocol/disco#info"},
|
||||
{Var: "http://jabber.org/protocol/disco#items"},
|
||||
{Var: "http://jabber.org/protocol/muc"},
|
||||
{Var: "http://jabber.org/protocol/caps"},
|
||||
},
|
||||
}
|
||||
testDiscoVer(t, &disco, "QgayPKawpkPSDYmwT/WM94uAlu0=")
|
||||
}
|
||||
|
||||
func TestDiscoVerPsi(t *testing.T) {
|
||||
disco := stanza.DiscoInfo{
|
||||
Identity: []stanza.Identity{
|
||||
{Name: "Psi 0.11", Category: "client", Type: "pc"},
|
||||
{Name: "Ψ 0.11", Category: "client", Type: "pc"},
|
||||
},
|
||||
Features: []stanza.Feature{
|
||||
{Var: "http://jabber.org/protocol/disco#info"},
|
||||
{Var: "http://jabber.org/protocol/disco#items"},
|
||||
{Var: "http://jabber.org/protocol/muc"},
|
||||
{Var: "http://jabber.org/protocol/caps"},
|
||||
},
|
||||
Form: &stanza.Form{
|
||||
Type: "result",
|
||||
Fields: []*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: "hidden", ValuesList: []string{"urn:xmpp:dataforms:softwareinfo"}},
|
||||
{Var: "ip_version", Type: "text-multi", ValuesList: []string{"ipv4", "ipv6"}},
|
||||
{Var: "os", ValuesList: []string{"Mac"}},
|
||||
{Var: "os_version", ValuesList: []string{"10.5.1"}},
|
||||
{Var: "software", ValuesList: []string{"Psi"}},
|
||||
{Var: "software_version", ValuesList: []string{"0.11"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
testDiscoVer(t, &disco, "MxdZjNKNku1+SiM9N92yqIK2HTQ=")
|
||||
}
|
||||
|
||||
func TestDiscoVerBombusMod(t *testing.T) {
|
||||
disco := stanza.DiscoInfo{
|
||||
Identity: []stanza.Identity{
|
||||
{Name: "BombusMod", Category: "client", Type: "mobile"},
|
||||
},
|
||||
Features: []stanza.Feature{
|
||||
{Var: "http://jabber.org/protocol/activity"},
|
||||
{Var: "http://jabber.org/protocol/activity+notify"},
|
||||
{Var: "http://jabber.org/protocol/caps"},
|
||||
{Var: "http://jabber.org/protocol/commands"},
|
||||
{Var: "http://jabber.org/protocol/disco#info"},
|
||||
{Var: "http://jabber.org/protocol/disco#items"},
|
||||
{Var: "http://jabber.org/protocol/rosterx"},
|
||||
{Var: "jabber:iq:last"},
|
||||
{Var: "jabber:iq:privacy"},
|
||||
{Var: "jabber:iq:roster"},
|
||||
{Var: "jabber:iq:time"},
|
||||
{Var: "jabber:iq:version"},
|
||||
{Var: "jabber:x:oob"},
|
||||
{Var: "urn:xmpp:ping"},
|
||||
{Var: "urn:xmpp:time"},
|
||||
},
|
||||
}
|
||||
testDiscoVer(t, &disco, "7Awj7pIUiI5UW/L2fdtzXZFQHsw=")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1174,33 +1174,12 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
|
|||
return
|
||||
}
|
||||
} else {
|
||||
disco.AddIdentity("", "account", "registered")
|
||||
if backend, ok := gateway.E2EE.Backend(); ok {
|
||||
disco.AddFeatures(backend.Namespaces().Disco...)
|
||||
}
|
||||
gateway.AddPMDiscoFeatures(disco)
|
||||
}
|
||||
disco.AddFeatures(stanza.NSMsgChatMarkers)
|
||||
disco.AddFeatures(stanza.NSMsgReceipts)
|
||||
// Jingle / JMI features so clients (e.g. Dino) treat the contact
|
||||
// as call-capable. Without these, JMI <proceed> never fires:
|
||||
// the client disco-info's the caller before engaging its call
|
||||
// state machine and silently drops the call when it sees no RTP
|
||||
// support advertised.
|
||||
disco.AddFeatures(
|
||||
"urn:xmpp:jingle:1",
|
||||
"urn:xmpp:jingle-message:0",
|
||||
"urn:xmpp:jingle:apps:rtp:1",
|
||||
"urn:xmpp:jingle:apps:rtp:audio",
|
||||
"urn:xmpp:jingle:transports:ice-udp:1",
|
||||
"urn:xmpp:jingle:apps:dtls:0",
|
||||
)
|
||||
} else {
|
||||
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
|
||||
disco.AddFeatures("jabber:iq:register")
|
||||
gateway.AddTransportDiscoFeatures(disco)
|
||||
}
|
||||
disco.AddFeatures(gateway.NSCommand)
|
||||
disco.AddFeatures("jabber:iq:version")
|
||||
disco.AddFeatures("urn:xmpp:time")
|
||||
gateway.AddCommonDiscoFeatures(disco)
|
||||
} else if di.Node == "x-roomuser-item" {
|
||||
bare, _, fromOk := gateway.SplitJID(iq.From)
|
||||
if fromOk {
|
||||
|
|
@ -1217,6 +1196,11 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ, di *stanza.DiscoInfo) {
|
|||
}
|
||||
} else if di.Node == "http://jabber.org/protocol/muc#traffic" {
|
||||
// noop yet, empty result as intended, TODO: add XHTML whenever supported
|
||||
} else if strings.HasPrefix(di.Node, gateway.CapsNode) {
|
||||
// Caps-aware client verifying the ver we advertised in presence (XEP-0115 S5.4).
|
||||
disco.SetNode(di.Node)
|
||||
gateway.AddPMDiscoFeatures(disco)
|
||||
gateway.AddCommonDiscoFeatures(disco)
|
||||
} else {
|
||||
chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue