plugin XEP-0115: Fix disco duplicate on caps node query + check own generation:

When caps node was requested, disco infos was returned from cache + infos from normal
disco gathering, resulting in doubled items and other entities rejecting the disco info.

This is fixed by subclassing the disco handler to add a trigger. This trigger is used by
XEP-0115 to short circuit the disco gathering and returned its cached data instead.

Our own hash is re-generated on each connection, and validated, so if there is a bug, this
helps see and catch it.
This commit is contained in:
Goffi 2026-05-04 15:33:18 +02:00
commit 18d3f0167f
3 changed files with 109 additions and 47 deletions

View file

@ -146,10 +146,8 @@ class TestXEP0115Handler:
assert disco.DiscoFeature(NS_ENTITY_CAPABILITY) in result
assert disco.DiscoFeature(NS_CAPS_OPTIMIZE) in result
def test_getDiscoInfo_caps_node_query(self, host, xep_0115):
"""getDiscoInfo returns cached disco info for caps node queries."""
handler = xep_0115.get_handler(MagicMock())
def test_disco_info_trigger_caps_node_query(self, host, xep_0115):
"""_disco_info_trigger returns cached disco info for caps node queries."""
# Create and cache some disco info
disco_infos = disco.DiscoInfo()
disco_infos.append(disco.DiscoIdentity("client", "pc", "Test Client"))
@ -161,9 +159,14 @@ class TestXEP0115Handler:
# Query with caps node format
node_identifier = f"{C.APP_URL}#{cap_hash}"
result = handler.getDiscoInfo(MagicMock(), MagicMock(), node_identifier)
cont, deferred = xep_0115._disco_info_trigger(
MagicMock(), MagicMock(), node_identifier
)
# Should return the cached disco info items
# Should not continue, return cached disco info
assert cont is False
assert deferred is not None
result = deferred.result
assert len(result) > 0
assert any(isinstance(item, disco.DiscoIdentity) for item in result)
assert any(isinstance(item, disco.DiscoFeature) for item in result)
@ -177,17 +180,26 @@ class TestXEP0115Handler:
assert disco.DiscoFeature("http://jabber.org/protocol/caps") in features
assert disco.DiscoFeature("http://jabber.org/protocol/disco#info") in features
def test_getDiscoInfo_unknown_caps_node(self, xep_0115):
"""getDiscoInfo returns caps features for unknown caps nodes."""
handler = xep_0115.get_handler(MagicMock())
def test_disco_info_trigger_unknown_caps_node(self, xep_0115):
"""_disco_info_trigger lets unknown caps nodes pass through."""
# Query with unknown hash
node_identifier = f"{C.APP_URL}#unknownhash123"
result = handler.getDiscoInfo(MagicMock(), MagicMock(), node_identifier)
cont, deferred = xep_0115._disco_info_trigger(
MagicMock(), MagicMock(), node_identifier
)
# Should return just the caps features, not cached info
assert disco.DiscoFeature(NS_ENTITY_CAPABILITY) in result
assert disco.DiscoFeature(NS_CAPS_OPTIMIZE) in result
# Should continue, let normal flow proceed
assert cont is True
assert deferred is None
def test_disco_info_trigger_non_caps_node(self, xep_0115):
"""_disco_info_trigger ignores non-caps nodes."""
cont, deferred = xep_0115._disco_info_trigger(
MagicMock(), MagicMock(), "some:other:node"
)
assert cont is True
assert deferred is None
def test_getDiscoItems_empty(self, xep_0115):
"""getDiscoItems returns empty list (caps nodes are virtual)."""