mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-10-07 16:51: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 {
|
if isMUC {
|
||||||
newArgs = append(newArgs, gateway.SPFullFrom(gateway.MUCJID(chatID)))
|
newArgs = append(newArgs, gateway.SPFullFrom(gateway.MUCJID(chatID)))
|
||||||
} else {
|
} else {
|
||||||
|
newArgs = append(newArgs, gateway.SPCaps(gateway.PMCapsVer()))
|
||||||
newArgs = gateway.SPAppendFrom(newArgs, chatID)
|
newArgs = gateway.SPAppendFrom(newArgs, chatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,13 @@ package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -53,6 +58,12 @@ const NodeAvatarMetadataNotify string = NodeAvatarMetadata + "+notify"
|
||||||
const NodeAvatarData string = "urn:xmpp:avatar:data"
|
const NodeAvatarData string = "urn:xmpp:avatar:data"
|
||||||
const NSCommand string = "http://jabber.org/protocol/commands"
|
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_MAM2 = "urn:xmpp:mam:2"
|
||||||
const NS_MAM1 = "urn:xmpp:mam:1"
|
const NS_MAM1 = "urn:xmpp:mam:1"
|
||||||
const NS_MAM0 = "urn:xmpp:mam:0"
|
const NS_MAM0 = "urn:xmpp:mam:0"
|
||||||
|
|
@ -722,6 +733,9 @@ var SPResource = args.NewString()
|
||||||
// SPImmed skips queueing
|
// SPImmed skips queueing
|
||||||
var SPImmed = args.NewBool(args.Default(true))
|
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
|
// SPMUCAffiliation is a XEP-0045 MUC affiliation
|
||||||
var SPMUCAffiliation = args.NewString()
|
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) {
|
if SPMUCAffiliation.IsSet(args) {
|
||||||
affiliation := SPMUCAffiliation.Get(args)
|
affiliation := SPMUCAffiliation.Get(args)
|
||||||
if affiliation != "" {
|
if affiliation != "" {
|
||||||
|
|
@ -1053,3 +1077,121 @@ func SendPubSubDeviceListNotification(component *xmpp.Component, jid string, cha
|
||||||
func InviteToMUC(chatID int64, jid string, component *xmpp.Component) {
|
func InviteToMUC(chatID int64, jid string, component *xmpp.Component) {
|
||||||
SendMUCInvite(jid, MUCNODE(chatID), component, Jid.Full())
|
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 (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
|
|
@ -52,3 +53,87 @@ func TestPresencePhoto(t *testing.T) {
|
||||||
presence := newPresence("from@test", "to@test", SPPhoto("01b87fcd030b72895ff8e88db57ec525450f000d"))
|
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>")
|
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
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
disco.AddIdentity("", "account", "registered")
|
gateway.AddPMDiscoFeatures(disco)
|
||||||
if backend, ok := gateway.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",
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
|
gateway.AddTransportDiscoFeatures(disco)
|
||||||
disco.AddFeatures("jabber:iq:register")
|
|
||||||
}
|
}
|
||||||
disco.AddFeatures(gateway.NSCommand)
|
gateway.AddCommonDiscoFeatures(disco)
|
||||||
disco.AddFeatures("jabber:iq:version")
|
|
||||||
disco.AddFeatures("urn:xmpp:time")
|
|
||||||
} else if di.Node == "x-roomuser-item" {
|
} else if di.Node == "x-roomuser-item" {
|
||||||
bare, _, fromOk := gateway.SplitJID(iq.From)
|
bare, _, fromOk := gateway.SplitJID(iq.From)
|
||||||
if fromOk {
|
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" {
|
} else if di.Node == "http://jabber.org/protocol/muc#traffic" {
|
||||||
// noop yet, empty result as intended, TODO: add XHTML whenever supported
|
// 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 {
|
} else {
|
||||||
chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To)
|
chatType, chatTypeErr := getTelegramChatType(iq.From, iq.To)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue