component pubsub: Integrate XEP-0499 extended discovery:

- Added `on_extended_disco()` handler.
- Added `with_parent=True` to all `get_pubsub_node()` calls to prevent lazy load errors in
  access checks.

rel 476

Registered XEP-0499 handler in `profile_connecting()` and added it to
plugin dependencies.
This commit is contained in:
Goffi 2026-04-11 02:11:03 +02:00
commit c89b91c2e9
3 changed files with 116 additions and 11 deletions

View file

@ -0,0 +1,26 @@
"""add "max_items" to PubsubNode
Revision ID: f9ce59cfc13f
Revises: ece2a6d7e4bb
Create Date: 2026-04-11 12:29:56.062028
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f9ce59cfc13f'
down_revision = 'ece2a6d7e4bb'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op:
batch_op.add_column(sa.Column('max_items', sa.Integer(), server_default=sa.text('0'), nullable=False))
def downgrade():
with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op:
batch_op.drop_column('max_items')

View file

@ -47,6 +47,13 @@ from libervia.backend.memory.sqla_mapping import (
from libervia.backend.plugins.plugin_xep_0167 import NS_AV_CONFERENCES
from libervia.backend.plugins.plugin_xep_0060 import NodeMetadata, XEP_0060
from libervia.backend.plugins.plugin_xep_0496 import LINK_VAR, PARENT_VAR, XEP_0496
from libervia.backend.plugins.plugin_xep_0499 import (
DiscoPubsubItem,
ExtDiscoOptions,
ExtDiscoResult,
DiscoPubsubNode,
XEP_0499,
)
from libervia.backend.tools.config import ComponentSettings
from libervia.backend.tools.xml_tools import element_copy
from tx_xmpp.pubsub import NS_PUBSUB_EVENT
@ -62,7 +69,7 @@ PLUGIN_INFO = {
C.PI_MODES: [C.PLUG_MODE_COMPONENT],
C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT,
C.PI_PROTOCOLS: [],
C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0496"],
C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0496", "XEP-0499"],
C.PI_RECOMMENDATIONS: [],
C.PI_MAIN: "PubsubComponent",
C.PI_HANDLER: C.BOOL_TRUE,
@ -174,7 +181,7 @@ class PubsubResource(pubsub.PubSubResource):
def __init__(self, service: "PubsubService") -> None:
super().__init__()
self.service = service
self.xep_0496 = cast(XEP_0496, service.client.host.plugins.get("XEP-0496"))
self.xep_0496 = cast(XEP_0496, G.host.plugins["XEP-0496"])
@property
def client(self) -> SatXMPPComponent:
@ -285,7 +292,7 @@ class PubsubResource(pubsub.PubSubResource):
client,
client.jid,
request.nodeIdentifier,
with_affiliations=True,
with_affiliations=True, with_parent=True,
with_subscriptions=True,
)
@ -673,8 +680,7 @@ class PubsubResource(pubsub.PubSubResource):
client,
client.jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
with_affiliations=True, with_parent=True,
with_link=True,
)
@ -744,8 +750,7 @@ class PubsubResource(pubsub.PubSubResource):
client,
client.jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
with_affiliations=True, with_parent=True,
with_link=True,
)
@ -904,7 +909,11 @@ class PubsubResource(pubsub.PubSubResource):
assert request.nodeIdentifier is not None
node = await G.storage.get_pubsub_node(
client, client.jid, request.nodeIdentifier, with_affiliations=True
client,
client.jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
)
if node is None:
@ -959,6 +968,7 @@ class PubsubResource(pubsub.PubSubResource):
client.jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
with_subscriptions=True,
)
@ -1030,6 +1040,7 @@ class PubsubResource(pubsub.PubSubResource):
client.jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
with_subscriptions=True,
)
@ -1073,6 +1084,7 @@ class PubsubResource(pubsub.PubSubResource):
client.jid,
request.nodeIdentifier,
with_affiliations=True,
with_parent=True,
with_subscriptions=True,
)
@ -1126,6 +1138,7 @@ class PubsubResource(pubsub.PubSubResource):
client.jid,
node_name,
with_affiliations=True,
with_parent=True,
with_subscriptions=True,
)
if fetched_node:
@ -1297,6 +1310,7 @@ class PubsubResource(pubsub.PubSubResource):
request.nodeIdentifier,
with_subscriptions=True,
with_affiliations=True,
with_parent=True,
)
if node is None:
@ -1561,6 +1575,71 @@ class PubsubService(rsm.PubSubService):
def client(self) -> SatXMPPComponent:
return self.component.client
async def on_extended_disco(
self,
client: SatXMPPComponent,
iq_elt: domish.Element,
ext_disco_options: ExtDiscoOptions,
) -> ExtDiscoResult:
"""Handle pubsub extended disco requests."""
query_elt = iq_elt.query
assert query_elt is not None
node_name = query_elt.getAttribute("node")
requestor_jid = client.jid
nodes = await G.storage.get_pubsub_nodes(
client,
service=client.jid,
node_name=node_name,
linked_node=ext_disco_options.linked_nodes,
depth=ext_disco_options.depth,
with_items="items" in ext_disco_options.type,
with_affiliations=True,
with_parent=True,
with_link=True,
)
result_items: list[DiscoPubsubNode | DiscoPubsubItem] = []
for node in nodes:
if ext_disco_options.depth == 0:
disco_node = DiscoPubsubNode.from_sqlalchemy(node, client.jid)
if "items" in ext_disco_options.type:
try:
disco_node.items = [
DiscoPubsubItem.from_sqlalchemy(item, node.name, client.jid)
for item in node.items
]
except Exception:
pass
else:
disco_node = DiscoPubsubNode.from_sqlalchemy_full_hierarchy(
node, client.jid
)
filtered_node = (
await self.component.ext_disco.filter_node_hierarchy_by_access(
client,
client.jid,
disco_node,
requestor_jid,
depth=ext_disco_options.depth,
linked_nodes=ext_disco_options.linked_nodes,
)
)
if filtered_node is None:
continue
if "nodes" in ext_disco_options.type:
result_items.append(filtered_node)
if "items" in ext_disco_options.type and filtered_node.items:
result_items.extend(filtered_node.items)
filtered_node.items = None
return ExtDiscoResult(result_items)
class PubsubComponent:
IMPORT_NAME = IMPORT_NAME
@ -1569,6 +1648,7 @@ class PubsubComponent:
def __init__(self, host):
self._clientt: SatXMPPComponent | None = None
self.pubsub_service = PubsubService(self)
self.ext_disco = cast(XEP_0499, host.plugins.get("XEP-0499"))
@property
def client(self) -> SatXMPPComponent:
@ -1580,6 +1660,7 @@ class PubsubComponent:
def profile_connecting(self, client: SatXMPPEntity) -> None:
self._client = cast(SatXMPPComponent, client)
self.ext_disco.register_handler(self.pubsub_service.on_extended_disco)
@implementer(itx_xmpp.IDisco)

View file

@ -53,9 +53,7 @@ PLUGIN_INFO = {
C.PI_TYPE: "XEP",
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: ["XEP-0499"],
C.PI_DEPENDENCIES: [
"XEP-0060", "XEP-0496"
],
C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0496"],
C.PI_RECOMMENDATIONS: [],
C.PI_MAIN: "XEP_0499",
C.PI_HANDLER: "yes",