diff --git a/libervia/backend/plugins/plugin_xep_0499.py b/libervia/backend/plugins/plugin_xep_0499.py
index b561f9c1..d3eea70a 100644
--- a/libervia/backend/plugins/plugin_xep_0499.py
+++ b/libervia/backend/plugins/plugin_xep_0499.py
@@ -17,26 +17,19 @@
# along with this program. If not, see .
from collections.abc import Callable, Coroutine, Iterable
-from typing import (
- TYPE_CHECKING,
- ClassVar,
- Final,
- Literal,
- NamedTuple,
- Self,
- cast,
-)
+from typing import ClassVar, Final, Literal, NamedTuple, Self, TYPE_CHECKING, cast
from pydantic import BaseModel, Field, RootModel, model_validator
from sqlalchemy.exc import SQLAlchemyError
from twisted.internet import defer
-from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber import xmlstream
+from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
-from tx_xmpp import disco, data_form, pubsub
+from tx_xmpp import data_form, disco, pubsub
from zope.interface import implementer
+from libervia.backend import G
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPComponent, SatXMPPEntity
@@ -45,6 +38,7 @@ from libervia.backend.core.log import getLogger
from libervia.backend.memory.sqla_mapping import PubsubItem, PubsubNode
from libervia.backend.models.types import JIDType
from libervia.backend.plugins.plugin_xep_0060 import NodeMetadata
+from libervia.backend.plugins.plugin_xep_0496 import LINK_VAR, PARENT_VAR, XEP_0496
from libervia.backend.tools import utils
if TYPE_CHECKING:
@@ -60,7 +54,7 @@ PLUGIN_INFO = {
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: ["XEP-0499"],
C.PI_DEPENDENCIES: [
- "XEP-0060",
+ "XEP-0060", "XEP-0496"
],
C.PI_RECOMMENDATIONS: [],
C.PI_MAIN: "XEP_0499",
@@ -71,9 +65,6 @@ PLUGIN_INFO = {
}
NS_PUBSUB_EXT_DISCO: Final = "urn:xmpp:pubsub-ext-disco:0"
-NS_PUBSUB_RELATIONSHIPS: Final = "urn:xmpp:pubsub-relationships:0"
-PARENT_VAR = f"{{{NS_PUBSUB_RELATIONSHIPS}}}parent"
-LINK_VAR = f"{{{NS_PUBSUB_RELATIONSHIPS}}}link"
class ExtDiscoMetadata(NodeMetadata):
@@ -95,18 +86,35 @@ class ExtDiscoMetadata(NodeMetadata):
metadata.link = form.get(LINK_VAR)
return metadata
- def to_data_form(self) -> data_form.Form:
+ def to_data_form(self, full_metadata: bool = False) -> data_form.Form:
"""Convert this instance to a data form.
+ @param full_metadata: If True, include all metadata fields. If False, only include
+ relationship fields (parent and link).
@return: Data form representation of this instance.
"""
- form = super().to_data_form()
+ if full_metadata:
+ form = super().to_data_form()
+ else:
+ form = data_form.Form(
+ formType="result", formNamespace=pubsub.NS_PUBSUB_META_DATA
+ )
+
if self.parent is not None:
form.addField(data_form.Field(var=PARENT_VAR, value=self.parent))
if self.link is not None:
form.addField(data_form.Field(var=LINK_VAR, value=self.link))
return form
+ def to_element(self, full_metadata: bool = False) -> domish.Element:
+ """Generate the element corresponding to this form.
+
+ @param full_metadata: If True, include all metadata fields. If False, only include
+ relationship fields (parent and link).
+ @return: XML element representation of this instance.
+ """
+ return self.to_data_form(full_metadata).toElement()
+
class ExtDiscoOptions(BaseModel):
"""Pydantic model for the pubsub extended discovery form fields."""
@@ -320,29 +328,27 @@ class DiscoPubsubNode(BaseModel):
return disco_node
- def to_element(self) -> domish.Element:
+ def to_element(self, full_metadata: bool = False) -> domish.Element:
item_elt = domish.Element((disco.NS_DISCO_ITEMS, "item"))
item_elt["jid"] = self.jid.full()
item_elt["node"] = self.name
- item_elt.addChild(self.metadata.to_element())
+ item_elt.addChild(self.metadata.to_element(full_metadata))
return item_elt
- def to_elements(
- self,
- ) -> list[domish.Element]:
+ def to_elements(self, full_metadata: bool = False) -> list[domish.Element]:
"""Return this elements and all its descendants and linking nodes.
@param parent_node: Parent of this node, None if it's a root node.
@param linked_node: Node this node is linking to, if any.
@return: This node, its descendants and linking nodes.
"""
- elements = [self.to_element()]
+ elements = [self.to_element(full_metadata)]
if self.linking_nodes is not None:
for linking_node in self.linking_nodes:
- elements.append(linking_node.to_element())
+ elements.append(linking_node.to_element(full_metadata))
if self.children is not None:
for child in self.children:
- elements.extend(child.to_elements())
+ elements.extend(child.to_elements(full_metadata))
return elements
@@ -455,11 +461,11 @@ class ExtDiscoResult(RootModel):
return cls(root=result_items)
- def to_elements(self) -> list[domish.Element]:
+ def to_elements(self, full_metadata: bool = False) -> list[domish.Element]:
elements = []
for item in self.root:
if isinstance(item, DiscoPubsubNode):
- elements.extend(item.to_elements())
+ elements.extend(item.to_elements(full_metadata))
else:
elements.append(item.to_element())
return elements
@@ -484,7 +490,6 @@ class XEP_0499:
self.handlers: list[RequestHandler] = []
host.register_namespace("pubsub-ext-disco", NS_PUBSUB_EXT_DISCO)
- host.register_namespace("pubsub-relationships", NS_PUBSUB_RELATIONSHIPS)
host.bridge.add_method(
"ps_disco_get",
".plugin",
@@ -493,6 +498,95 @@ class XEP_0499:
method=self._disco_get,
async_=True,
)
+ self._xep_0496 = cast(XEP_0496, host.plugins["XEP-0496"])
+
+ async def filter_node_hierarchy_by_access(
+ self,
+ client: SatXMPPEntity,
+ service: JID,
+ node: DiscoPubsubNode,
+ entity_jid: JID,
+ depth: int = 0,
+ linked_nodes: bool = False,
+ current_depth: int = 0,
+ access_cache: dict[str, bool] | None = None,
+ ) -> DiscoPubsubNode | None:
+ """Filter a node hierarchy based on access control and depth.
+
+ @param client: Client instance.
+ @param service: The pubsub service JID.
+ @param node: The DiscoPubsubNode to filter.
+ @param entity_jid: The entity JID to check access for.
+ @param depth: Maximum depth to traverse (0 = only this node).
+ @param linked_nodes: Whether to include linking nodes.
+ @param current_depth: Current depth in the traversal.
+ @param access_cache: Cache of access results for nodes already checked.
+ @return: Filtered DiscoPubsubNode or None if not accessible.
+ """
+ if access_cache is None:
+ access_cache = {}
+
+ if node.name in access_cache:
+ if not access_cache[node.name]:
+ return None
+ return DiscoPubsubNode(jid=node.jid, name=node.name, metadata=node.metadata)
+
+ pubsub_node = await G.storage.get_pubsub_node(
+ client, service, node.name, with_affiliations=True, with_parent=True
+ )
+ if pubsub_node is None:
+ access_cache[node.name] = False
+ return None
+
+ has_access = await self._xep_0496.has_access(
+ client, service, pubsub_node, entity_jid
+ )
+ access_cache[node.name] = has_access
+ if not has_access:
+ return None
+
+ filtered_node = DiscoPubsubNode(
+ jid=node.jid, name=node.name, metadata=node.metadata
+ )
+
+ if current_depth < depth:
+ if node.children is not None:
+ filtered_children: list[DiscoPubsubNode] = []
+ for child in node.children:
+ filtered_child = await self.filter_node_hierarchy_by_access(
+ client,
+ service,
+ child,
+ entity_jid,
+ depth,
+ linked_nodes,
+ current_depth + 1,
+ access_cache,
+ )
+ if filtered_child is not None:
+ filtered_children.append(filtered_child)
+ if filtered_children:
+ filtered_node.children = filtered_children
+
+ if linked_nodes and node.linking_nodes is not None:
+ filtered_linking: list[DiscoPubsubNode] = []
+ for linking in node.linking_nodes:
+ filtered_linking_node = await self.filter_node_hierarchy_by_access(
+ client,
+ service,
+ linking,
+ entity_jid,
+ depth,
+ linked_nodes,
+ current_depth + 1,
+ access_cache,
+ )
+ if filtered_linking_node is not None:
+ filtered_linking.append(filtered_linking_node)
+ if filtered_linking:
+ filtered_node.linking_nodes = filtered_linking
+
+ return filtered_node
def get_handler(self, client: SatXMPPEntity) -> "PubsubExtDiscoHandler":
return PubsubExtDiscoHandler(self)
@@ -564,7 +658,7 @@ class XEP_0499:
result_query_elt = iq_result_elt.addElement((disco.NS_DISCO_ITEMS, "query"))
if query_elt.hasAttribute("node"):
result_query_elt["node"] = query_elt["node"]
- for elt in disco_result.to_elements():
+ for elt in disco_result.to_elements(ext_disco_options.full_metadata):
result_query_elt.addChild(elt)
client.send(iq_result_elt)