diff --git a/libervia/backend/memory/migration/versions/f9ce59cfc13f_add_max_items_to_pubsubnode.py b/libervia/backend/memory/migration/versions/f9ce59cfc13f_add_max_items_to_pubsubnode.py new file mode 100644 index 00000000..ea7d33e7 --- /dev/null +++ b/libervia/backend/memory/migration/versions/f9ce59cfc13f_add_max_items_to_pubsubnode.py @@ -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') diff --git a/libervia/backend/plugins/plugin_comp_pubsub/__init__.py b/libervia/backend/plugins/plugin_comp_pubsub/__init__.py index 3f6c1b2c..65ebb9b8 100644 --- a/libervia/backend/plugins/plugin_comp_pubsub/__init__.py +++ b/libervia/backend/plugins/plugin_comp_pubsub/__init__.py @@ -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) diff --git a/libervia/backend/plugins/plugin_xep_0499.py b/libervia/backend/plugins/plugin_xep_0499.py index d3eea70a..c982fa40 100644 --- a/libervia/backend/plugins/plugin_xep_0499.py +++ b/libervia/backend/plugins/plugin_xep_0499.py @@ -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",